From f3af774deaff99d6a5e2af7f77dc6bf520ff1c84 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Thu, 11 Sep 2025 13:50:15 +0400 Subject: [PATCH 01/16] feat: add storage utilities and builder methods for hosting ctx --- crates/testing/Cargo.toml | 2 ++ crates/testing/src/host.rs | 35 +++++++++++++++++--- crates/testing/src/lib.rs | 4 +-- crates/testing/src/utils.rs | 65 +++++++++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 crates/testing/src/utils.rs diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml index e4ab6c1df..cb03dee43 100644 --- a/crates/testing/Cargo.toml +++ b/crates/testing/Cargo.toml @@ -19,6 +19,8 @@ revm = { workspace = true } fluentbase-revm = { workspace = true } rwasm = { workspace = true } hex = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } [features] default = ["std"] diff --git a/crates/testing/src/host.rs b/crates/testing/src/host.rs index e1b778dc3..ccef2dc16 100644 --- a/crates/testing/src/host.rs +++ b/crates/testing/src/host.rs @@ -1,11 +1,10 @@ use core::cell::RefCell; use fluentbase_runtime::{RuntimeContext, RuntimeContextWrapper}; -use fluentbase_sdk::syscall::SyscallResult; use fluentbase_sdk::{ - bytes::Buf, calc_create4_address, native_api::NativeAPI, Address, Bytes, ContextReader, - ContractContextV1, ExitCode, IsAccountEmpty, IsAccountOwnable, IsColdAccess, MetadataAPI, - MetadataStorageAPI, SharedAPI, SharedContextInputV1, StorageAPI, B256, - BN254_G1_POINT_COMPRESSED_SIZE, BN254_G1_POINT_DECOMPRESSED_SIZE, + bytes::Buf, calc_create4_address, native_api::NativeAPI, syscall::SyscallResult, Address, + Bytes, ContextReader, ContractContextV1, ExitCode, IsAccountEmpty, IsAccountOwnable, + IsColdAccess, MetadataAPI, MetadataStorageAPI, SharedAPI, SharedContextInputV1, StorageAPI, + B256, BN254_G1_POINT_COMPRESSED_SIZE, BN254_G1_POINT_DECOMPRESSED_SIZE, BN254_G2_POINT_COMPRESSED_SIZE, BN254_G2_POINT_DECOMPRESSED_SIZE, FUEL_DENOM_RATE, U256, }; use hashbrown::HashMap; @@ -44,6 +43,32 @@ impl HostTestingContext { .change_input(input.into()); self } + /// Sets the initial storage state + pub fn with_storage(self, storage: HashMap<(Address, U256), U256>) -> Self { + self.inner.borrow_mut().persistent_storage = storage; + self + } + + /// Merges storage entries + pub fn with_storage_entries( + self, + entries: impl IntoIterator, + ) -> Self { + self.inner.borrow_mut().persistent_storage.extend(entries); + self + } + + /// Sets storage for a specific contract + pub fn with_contract_storage(self, contract: Address, slots: HashMap) -> Self { + for (slot, value) in slots { + self.inner + .borrow_mut() + .persistent_storage + .insert((contract, slot), value); + } + self + } + pub fn set_ownable_account_address(&mut self, address: Address) { self.inner.borrow_mut().ownable_account_address = Some(address); } diff --git a/crates/testing/src/lib.rs b/crates/testing/src/lib.rs index 8a7bab680..4b80f8234 100644 --- a/crates/testing/src/lib.rs +++ b/crates/testing/src/lib.rs @@ -1,8 +1,8 @@ mod evm; mod host; +mod utils; pub use evm::*; pub use fluentbase_sdk::include_this_wasm; pub use host::*; - -extern crate alloc; +pub use utils::*; diff --git a/crates/testing/src/utils.rs b/crates/testing/src/utils.rs new file mode 100644 index 000000000..ab33cc8cf --- /dev/null +++ b/crates/testing/src/utils.rs @@ -0,0 +1,65 @@ +use core::str::FromStr; +use fluentbase_sdk::{Address, U256}; +use hashbrown::HashMap; +use serde_json::Value; + +/// Creates storage HashMap from JSON fixture +/// +/// Expected JSON format: {"contract": {"slot":"value"}} +/// ```json +/// { +/// "0x123": { +/// "0x00": "0xabc", +/// "0x01": "0xdef" +/// } +/// } +/// ``` +pub fn storage_from_fixture(json: &str) -> HashMap<(Address, U256), U256> { + let fixture: Value = serde_json::from_str(json).expect("Invalid JSON"); + let mut storage = HashMap::new(); + + if let Some(contracts) = fixture.as_object() { + for (address_str, slots) in contracts { + let address = Address::from_str(address_str).expect("Invalid address"); + + if let Some(slots_map) = slots.as_object() { + for (slot_str, value_str) in slots_map { + let slot = U256::from_str(slot_str).expect("Invalid slot"); + let value = U256::from_str(value_str.as_str().expect("Value must be string")) + .expect("Invalid value"); + + storage.insert((address, slot), value); + } + } + } + } + + storage +} + +/// Pretty-prints storage entries grouped by contract address and sorted by slot. +pub fn format_storage(storage: &HashMap<(Address, U256), U256>) -> String { + if storage.is_empty() { + return " (empty)".to_string(); + } + + let mut entries: Vec<_> = storage.iter().collect(); + entries.sort_by_key(|((addr, slot), _)| (*addr, *slot)); + + let mut result = String::new(); + let mut current_addr: Option
= None; + + for ((addr, slot), value) in entries { + if current_addr != Some(*addr) { + if current_addr.is_some() { + result.push('\n'); + } + result.push_str(&format!(" Contract 0x{:040x}:\n", addr)); + current_addr = Some(*addr); + } + + result.push_str(&format!(" Slot 0x{:064x}: 0x{:064x}\n", slot, value)); + } + + result +} From acb1347d806300aa9f66c4a0e13a5c406436a7b3 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Thu, 11 Sep 2025 14:04:12 +0400 Subject: [PATCH 02/16] refactor(sdk): rename storage to storage_legacy --- Cargo.lock | 2 ++ contracts/Cargo.lock | 2 ++ ...e_core__storage__tests__array_storage.snap | 12 +++++++---- ...__storage__tests__fixed_bytes_storage.snap | 20 +++++++++---------- ...core__storage__tests__mapping_storage.snap | 18 ++++++++++------- ...re__storage__tests__primitive_storage.snap | 18 +++++++++++------ crates/sdk-derive/derive-core/src/storage.rs | 20 +++++++++---------- crates/sdk/src/lib.rs | 3 ++- .../sdk/src/{storage.rs => storage_legacy.rs} | 0 9 files changed, 57 insertions(+), 38 deletions(-) rename crates/sdk/src/{storage.rs => storage_legacy.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index c4821d69f..0d4507ab5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2341,6 +2341,8 @@ dependencies = [ "hex", "revm", "rwasm", + "serde", + "serde_json", ] [[package]] diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index c4fec6878..1fe68e6fa 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -2055,6 +2055,8 @@ dependencies = [ "hex", "revm", "rwasm", + "serde", + "serde_json", ] [[package]] diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__array_storage.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__array_storage.snap index 5145a7df5..3890bdc7a 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__array_storage.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__array_storage.snap @@ -9,11 +9,13 @@ impl Arr { ]); fn get(sdk: &SDK, arg0: U256) -> U256 { let key = Self::key(sdk, arg0); - >::get(sdk, key) + >::get(sdk, key) } fn set(sdk: &mut SDK, arg0: U256, value: U256) { let key = Self::key(sdk, arg0); - >::set(sdk, key, value) + >::set(sdk, key, value) } fn key( sdk: &SDK, @@ -41,7 +43,7 @@ impl NestedArr { arg2: U256, ) -> Address { let key = Self::key(sdk, arg0, arg1, arg2); -
>::get(sdk, key) +
>::get(sdk, key) } fn set( sdk: &mut SDK, @@ -51,7 +53,9 @@ impl NestedArr { value: Address, ) { let key = Self::key(sdk, arg0, arg1, arg2); -
>::set(sdk, key, value) +
>::set(sdk, key, value) } fn key( sdk: &SDK, diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__fixed_bytes_storage.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__fixed_bytes_storage.snap index 061148751..8163c0a9f 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__fixed_bytes_storage.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__fixed_bytes_storage.snap @@ -11,13 +11,13 @@ impl CustomBytes1 { let key = Self::key(sdk); as fluentbase_sdk::storage::DirectStorage>::get(sdk, key) + > as fluentbase_sdk::storage_legacy::DirectStorage>::get(sdk, key) } fn set(sdk: &mut SDK, value: FixedBytes<32usize>) { let key = Self::key(sdk); as fluentbase_sdk::storage::DirectStorage>::set(sdk, key, value) + > as fluentbase_sdk::storage_legacy::DirectStorage>::set(sdk, key, value) } fn key(_sdk: &SDK) -> fluentbase_sdk::U256 { Self::SLOT @@ -32,13 +32,13 @@ impl CustomBytes2 { let key = Self::key(sdk); as fluentbase_sdk::storage::DirectStorage>::get(sdk, key) + > as fluentbase_sdk::storage_legacy::DirectStorage>::get(sdk, key) } fn set(sdk: &mut SDK, value: FixedBytes<32usize>) { let key = Self::key(sdk); as fluentbase_sdk::storage::DirectStorage>::set(sdk, key, value) + > as fluentbase_sdk::storage_legacy::DirectStorage>::set(sdk, key, value) } fn key(_sdk: &SDK) -> fluentbase_sdk::U256 { Self::SLOT @@ -53,17 +53,17 @@ impl CustomBytes1 { let key = Self::key(sdk); as fluentbase_sdk::storage::StorageValueSolidity< + > as fluentbase_sdk::storage_legacy::StorageValueSolidity< SDK, FixedBytes<321usize>, >>::get(sdk, key) } fn set(sdk: &mut SDK, value: FixedBytes<321usize>) { - use fluentbase_sdk::storage::StorageValueSolidity; + use fluentbase_sdk::storage_legacy::StorageValueSolidity; let key = Self::key(sdk); as fluentbase_sdk::storage::StorageValueSolidity< + > as fluentbase_sdk::storage_legacy::StorageValueSolidity< SDK, FixedBytes<321usize>, >>::set(sdk, key, value.clone()); @@ -81,17 +81,17 @@ impl CustomBytes2 { let key = Self::key(sdk); as fluentbase_sdk::storage::StorageValueSolidity< + > as fluentbase_sdk::storage_legacy::StorageValueSolidity< SDK, FixedBytes<321usize>, >>::get(sdk, key) } fn set(sdk: &mut SDK, value: FixedBytes<321usize>) { - use fluentbase_sdk::storage::StorageValueSolidity; + use fluentbase_sdk::storage_legacy::StorageValueSolidity; let key = Self::key(sdk); as fluentbase_sdk::storage::StorageValueSolidity< + > as fluentbase_sdk::storage_legacy::StorageValueSolidity< SDK, FixedBytes<321usize>, >>::set(sdk, key, value.clone()); diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__mapping_storage.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__mapping_storage.snap index 14fd16e32..20242436d 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__mapping_storage.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__mapping_storage.snap @@ -9,11 +9,13 @@ impl Balance { ]); fn get(sdk: &SDK, arg0: Address) -> U256 { let key = Self::key(sdk, arg0); - >::get(sdk, key) + >::get(sdk, key) } fn set(sdk: &mut SDK, arg0: Address, value: U256) { let key = Self::key(sdk, arg0); - >::set(sdk, key, value) + >::set(sdk, key, value) } fn calculate_keys( sdk: &SDK, @@ -63,15 +65,15 @@ impl ArbitraryData { ]); fn get(sdk: &SDK, arg0: Address) -> Bytes { let key = Self::key(sdk, arg0); - >::get(sdk, key) } fn set(sdk: &mut SDK, arg0: Address, value: Bytes) { - use fluentbase_sdk::storage::StorageValueSolidity; + use fluentbase_sdk::storage_legacy::StorageValueSolidity; let key = Self::key(sdk, arg0); - >::set(sdk, key, value.clone()); @@ -128,7 +130,7 @@ impl Allowance { arg1: Address, ) -> U256 { let key = Self::key(sdk, arg0, arg1); - >::get(sdk, key) + >::get(sdk, key) } fn set( sdk: &mut SDK, @@ -137,7 +139,9 @@ impl Allowance { value: U256, ) { let key = Self::key(sdk, arg0, arg1); - >::set(sdk, key, value) + >::set(sdk, key, value) } fn calculate_keys( sdk: &SDK, diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__primitive_storage.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__primitive_storage.snap index 598fca407..2bdb757d9 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__primitive_storage.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__primitive_storage.snap @@ -9,11 +9,13 @@ impl Owner { ]); fn get(sdk: &SDK) -> Address { let key = Self::key(sdk); -
>::get(sdk, key) +
>::get(sdk, key) } fn set(sdk: &mut SDK, value: Address) { let key = Self::key(sdk); -
>::set(sdk, key, value) +
>::set(sdk, key, value) } fn key(_sdk: &SDK) -> fluentbase_sdk::U256 { Self::SLOT @@ -26,11 +28,13 @@ impl Paused { ]); fn get(sdk: &SDK) -> bool { let key = Self::key(sdk); - >::get(sdk, key) + >::get(sdk, key) } fn set(sdk: &mut SDK, value: bool) { let key = Self::key(sdk); - >::set(sdk, key, value) + >::set(sdk, key, value) } fn key(_sdk: &SDK) -> fluentbase_sdk::U256 { Self::SLOT @@ -43,11 +47,13 @@ impl TotalSupply { ]); fn get(sdk: &SDK) -> U256 { let key = Self::key(sdk); - >::get(sdk, key) + >::get(sdk, key) } fn set(sdk: &mut SDK, value: U256) { let key = Self::key(sdk); - >::set(sdk, key, value) + >::set(sdk, key, value) } fn key(_sdk: &SDK) -> fluentbase_sdk::U256 { Self::SLOT diff --git a/crates/sdk-derive/derive-core/src/storage.rs b/crates/sdk-derive/derive-core/src/storage.rs index 4f83f774b..9a1f93cf4 100644 --- a/crates/sdk-derive/derive-core/src/storage.rs +++ b/crates/sdk-derive/derive-core/src/storage.rs @@ -546,14 +546,14 @@ impl StorageSlot { quote! { fn get(#get_args) -> #output { let key = Self::key(sdk); - <#output as fluentbase_sdk::storage::DirectStorage>::get(sdk, key) + <#output as fluentbase_sdk::storage_legacy::DirectStorage>::get(sdk, key) } } } else { quote! { fn get(#get_args) -> #output { let key = Self::key(sdk, #(#arg_names),*); - <#output as fluentbase_sdk::storage::DirectStorage>::get(sdk, key) + <#output as fluentbase_sdk::storage_legacy::DirectStorage>::get(sdk, key) } } } @@ -561,14 +561,14 @@ impl StorageSlot { quote! { fn get(#get_args) -> #output { let key = Self::key(sdk); - <#output as fluentbase_sdk::storage::StorageValueSolidity>::get(sdk, key) + <#output as fluentbase_sdk::storage_legacy::StorageValueSolidity>::get(sdk, key) } } } else { quote! { fn get(#get_args) -> #output { let key = Self::key(sdk, #(#arg_names),*); - <#output as fluentbase_sdk::storage::StorageValueSolidity>::get(sdk, key) + <#output as fluentbase_sdk::storage_legacy::StorageValueSolidity>::get(sdk, key) } } } @@ -594,14 +594,14 @@ impl StorageSlot { quote! { fn set(#set_args) { let key = Self::key(sdk); - <#output as fluentbase_sdk::storage::DirectStorage>::set(sdk, key, value) + <#output as fluentbase_sdk::storage_legacy::DirectStorage>::set(sdk, key, value) } } } else { quote! { fn set(#set_args) { let key = Self::key(sdk, #(#arg_names),*); - <#output as fluentbase_sdk::storage::DirectStorage>::set(sdk, key, value) + <#output as fluentbase_sdk::storage_legacy::DirectStorage>::set(sdk, key, value) } } } @@ -610,17 +610,17 @@ impl StorageSlot { if arguments.is_empty() { quote! { fn set(#set_args) { - use fluentbase_sdk::storage::StorageValueSolidity; + use fluentbase_sdk::storage_legacy::StorageValueSolidity; let key = Self::key(sdk); - <#output as fluentbase_sdk::storage::StorageValueSolidity>::set(sdk, key, value.clone()); + <#output as fluentbase_sdk::storage_legacy::StorageValueSolidity>::set(sdk, key, value.clone()); } } } else { quote! { fn set(#set_args) { - use fluentbase_sdk::storage::StorageValueSolidity; + use fluentbase_sdk::storage_legacy::StorageValueSolidity; let key = Self::key(sdk, #(#arg_names),*); - <#output as fluentbase_sdk::storage::StorageValueSolidity>::set(sdk, key, value.clone()); + <#output as fluentbase_sdk::storage_legacy::StorageValueSolidity>::set(sdk, key, value.clone()); } } } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 0610deed2..f2c761956 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -14,7 +14,8 @@ pub mod panic; #[cfg(not(feature = "std"))] pub mod rwasm; pub mod shared; -pub mod storage; +#[deprecated(note = "Use `fluentbase_sdk::storage` instead", since = "0.4.5-dev")] +pub mod storage_legacy; pub use allocator::*; pub use fluentbase_codec as codec; diff --git a/crates/sdk/src/storage.rs b/crates/sdk/src/storage_legacy.rs similarity index 100% rename from crates/sdk/src/storage.rs rename to crates/sdk/src/storage_legacy.rs From 852670b5a6aba6858ee56d2d5cfa735b8d47de1d Mon Sep 17 00:00:00 2001 From: d1r1 Date: Thu, 11 Sep 2025 15:11:26 +0400 Subject: [PATCH 03/16] feat: add solidity-compatible storage --- crates/sdk/src/lib.rs | 1 + crates/sdk/src/storage/array.rs | 263 +++++++++++++ crates/sdk/src/storage/bytes.rs | 581 ++++++++++++++++++++++++++++ crates/sdk/src/storage/composite.rs | 70 ++++ crates/sdk/src/storage/map.rs | 494 +++++++++++++++++++++++ crates/sdk/src/storage/mock.rs | 70 ++++ crates/sdk/src/storage/mod.rs | 224 +++++++++++ crates/sdk/src/storage/primitive.rs | 299 ++++++++++++++ crates/sdk/src/storage/vec.rs | 339 ++++++++++++++++ 9 files changed, 2341 insertions(+) create mode 100644 crates/sdk/src/storage/array.rs create mode 100644 crates/sdk/src/storage/bytes.rs create mode 100644 crates/sdk/src/storage/composite.rs create mode 100644 crates/sdk/src/storage/map.rs create mode 100644 crates/sdk/src/storage/mock.rs create mode 100644 crates/sdk/src/storage/mod.rs create mode 100644 crates/sdk/src/storage/primitive.rs create mode 100644 crates/sdk/src/storage/vec.rs diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index f2c761956..1023f8ba1 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -16,6 +16,7 @@ pub mod rwasm; pub mod shared; #[deprecated(note = "Use `fluentbase_sdk::storage` instead", since = "0.4.5-dev")] pub mod storage_legacy; +pub mod storage; pub use allocator::*; pub use fluentbase_codec as codec; diff --git a/crates/sdk/src/storage/array.rs b/crates/sdk/src/storage/array.rs new file mode 100644 index 000000000..2fe11139a --- /dev/null +++ b/crates/sdk/src/storage/array.rs @@ -0,0 +1,263 @@ +use crate::{ + storage::{ArrayAccess, StorageDescriptor, StorageLayout}, + U256, +}; +use core::marker::PhantomData; + +// --- 1. Array Descriptor --- + +/// A descriptor for a fixed-size array in storage. +/// Arrays always start at slot boundaries (offset = 0). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct StorageArray { + base_slot: U256, + _marker: PhantomData, +} + +impl StorageArray { + /// Creates a new array descriptor at the given base slot. + pub const fn new(base_slot: U256) -> Self { + Self { + base_slot, + _marker: PhantomData, + } + } +} + +impl StorageDescriptor for StorageArray { + fn new(slot: U256, offset: u8) -> Self { + debug_assert_eq!(offset, 0, "arrays always start at slot boundary"); + Self { + base_slot: slot, + _marker: PhantomData, + } + } + + fn slot(&self) -> U256 { + self.base_slot + } + + fn offset(&self) -> u8 { + 0 + } +} + +// --- 2. ArrayAccess Implementation --- + +impl ArrayAccess for StorageArray +where + T::Descriptor: StorageDescriptor, +{ + fn at(&self, index: usize) -> T::Accessor { + assert!(index < N, "array index out of bounds"); + let (slot, offset) = if T::ENCODED_SIZE <= 32 && T::REQUIRED_SLOTS == 0 { + let element_size = T::ENCODED_SIZE; + let elements_per_slot = 32 / element_size; + + let slot_index = index / elements_per_slot; + let position_in_slot = index % elements_per_slot; + + // Solidity packs from right to left + // the First element goes to the rightmost position + // offset = start position from left edge + // For the element at position 0 (rightmost): offset = 32 - element_size + // For the element at position 1: offset = 32-2*element_size + let offset = (32 - (position_in_slot + 1) * element_size) as u8; + + (self.base_slot + U256::from(slot_index), offset) + } else { + // Non-packable types + let slots = if T::REQUIRED_SLOTS == 0 { + 1 + } else { + T::REQUIRED_SLOTS + }; + (self.base_slot + U256::from(index * slots), 0) + }; + + T::access(T::Descriptor::new(slot, offset)) + } +} + +// --- 3. StorageLayout Implementation --- + +impl StorageLayout for StorageArray +where + T::Descriptor: StorageDescriptor, +{ + type Descriptor = StorageArray; + + // Array acts as its own accessor - no separate accessor type needed + type Accessor = Self; + + const REQUIRED_SLOTS: usize = { + if T::REQUIRED_SLOTS == 0 { + let elements_per_slot = 32 / T::ENCODED_SIZE; + N.div_ceil(elements_per_slot) + } else { + N * T::REQUIRED_SLOTS + } + }; + + const ENCODED_SIZE: usize = Self::REQUIRED_SLOTS * 32; + + fn access(descriptor: Self::Descriptor) -> Self::Accessor { + // Return the descriptor itself as it acts as the accessor + descriptor + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::mock::MockStorage; + use crate::storage::primitive::StoragePrimitive; + use crate::storage::PrimitiveAccess; + + #[test] + fn test_u256_array_no_packing() { + let mut sdk = MockStorage::new(); + let array = StorageArray::, 3>::new(U256::from(10)); + + // Each U256 takes a full slot - no packing possible + assert_eq!( + , 3> as StorageLayout>::REQUIRED_SLOTS, + 3 + ); + + // Direct array access without intermediate accessor + array.at(0).set(&mut sdk, U256::from(100)); + array.at(1).set(&mut sdk, U256::from(200)); + array.at(2).set(&mut sdk, U256::from(300)); + + // Verify each value is in its own slot + assert_eq!(sdk.get_slot(U256::from(10)), U256::from(100)); + assert_eq!(sdk.get_slot(U256::from(11)), U256::from(200)); + assert_eq!(sdk.get_slot(U256::from(12)), U256::from(300)); + } + + #[test] + fn test_u64_array_packing() { + let mut sdk = MockStorage::new(); + let array = StorageArray::, 4>::new(U256::from(20)); + + // 4 u64 values (8 bytes each) should pack into 1 slot (32 bytes) + assert_eq!( + , 4> as StorageLayout>::REQUIRED_SLOTS, + 1 + ); + + array.at(0).set(&mut sdk, 0x1111111111111111u64); + array.at(1).set(&mut sdk, 0x2222222222222222u64); + array.at(2).set(&mut sdk, 0x3333333333333333u64); + array.at(3).set(&mut sdk, 0x4444444444444444u64); + + // All values packed in slot 20, from right to left + let expected = "4444444444444444333333333333333322222222222222221111111111111111"; + assert_eq!(sdk.get_slot_hex(U256::from(20)), expected); + } + + #[test] + fn test_bool_array_packing() { + let mut sdk = MockStorage::new(); + let array = StorageArray::, 32>::new(U256::from(30)); + + // 32 bools (1 byte each) should pack into 1 slot + assert_eq!( + , 32> as StorageLayout>::REQUIRED_SLOTS, + 1 + ); + + // Set some values + array.at(0).set(&mut sdk, true); + array.at(1).set(&mut sdk, false); + array.at(2).set(&mut sdk, true); + array.at(31).set(&mut sdk, true); + + // Read back + assert!(array.at(0).get(&sdk)); + assert!(!array.at(1).get(&sdk)); + assert!(array.at(2).get(&sdk)); + assert!(array.at(31).get(&sdk)); + } + + #[test] + fn test_nested_array() { + let mut sdk = MockStorage::new(); + // Array of arrays: 3 arrays, each containing 2 U256 values + let array = StorageArray::, 2>, 3>::new(U256::from(50)); + + // Should take 6 slots total (3 arrays * 2 slots each) + assert_eq!( + , 2>, 3> as StorageLayout>::REQUIRED_SLOTS, + 6 + ); + + // Access nested elements + array.at(0).at(0).set(&mut sdk, U256::from(100)); + array.at(0).at(1).set(&mut sdk, U256::from(101)); + array.at(1).at(0).set(&mut sdk, U256::from(200)); + array.at(1).at(1).set(&mut sdk, U256::from(201)); + array.at(2).at(0).set(&mut sdk, U256::from(300)); + array.at(2).at(1).set(&mut sdk, U256::from(301)); + + // Verify storage layout - sequential slots for nested arrays + assert_eq!(sdk.get_slot(U256::from(50)), U256::from(100)); // array[0][0] + assert_eq!(sdk.get_slot(U256::from(51)), U256::from(101)); // array[0][1] + assert_eq!(sdk.get_slot(U256::from(52)), U256::from(200)); // array[1][0] + assert_eq!(sdk.get_slot(U256::from(53)), U256::from(201)); // array[1][1] + assert_eq!(sdk.get_slot(U256::from(54)), U256::from(300)); // array[2][0] + assert_eq!(sdk.get_slot(U256::from(55)), U256::from(301)); // array[2][1] + } + + #[test] + #[should_panic(expected = "array index out of bounds")] + fn test_bounds_check() { + let array = StorageArray::, 3>::new(U256::from(60)); + + // This should panic - index 3 is out of bounds for an array of size 3 + array.at(3); + } + + #[test] + fn test_packed_array_multiple_slots() { + let mut sdk = MockStorage::new(); + let array = StorageArray::, 5>::new(U256::from(70)); + + // 5 u64 values need 2 slots (4 in the first slot, 1 in the second) + assert_eq!( + , 5> as StorageLayout>::REQUIRED_SLOTS, + 2 + ); + + array.at(0).set(&mut sdk, 0x1111111111111111u64); + array.at(1).set(&mut sdk, 0x2222222222222222u64); + array.at(2).set(&mut sdk, 0x3333333333333333u64); + array.at(3).set(&mut sdk, 0x4444444444444444u64); + array.at(4).set(&mut sdk, 0x5555555555555555u64); + + // First 4 packed in slot 70 + let expected_slot_70 = "4444444444444444333333333333333322222222222222221111111111111111"; + assert_eq!(sdk.get_slot_hex(U256::from(70)), expected_slot_70); + + // Fifth element in slot 71 + let expected_slot_71 = "0000000000000000000000000000000000000000000000005555555555555555"; + assert_eq!(sdk.get_slot_hex(U256::from(71)), expected_slot_71); + } + + #[test] + fn test_array_get_operations() { + let mut sdk = MockStorage::new(); + let array = StorageArray::, 8>::new(U256::from(80)); + + // Set values first + for i in 0..8 { + array.at(i).set(&mut sdk, (i as u32 + 1) * 100); + } + + // Test get operations + for i in 0..8 { + assert_eq!(array.at(i).get(&sdk), (i as u32 + 1) * 100); + } + } +} diff --git a/crates/sdk/src/storage/bytes.rs b/crates/sdk/src/storage/bytes.rs new file mode 100644 index 000000000..c23b92fc8 --- /dev/null +++ b/crates/sdk/src/storage/bytes.rs @@ -0,0 +1,581 @@ +use crate::{ + keccak256, + storage::{BytesAccess, StorageDescriptor, StorageLayout, StorageOps}, + B256, U256, +}; +use alloc::{string::String, vec::Vec}; +use core::marker::PhantomData; +use fluentbase_types::StorageAPI; + +// --- 1. Bytes Descriptor --- + +/// A descriptor for dynamic byte arrays in storage. +/// Follows Solidity's bytes/string storage optimization: +/// - Short (< 32 bytes): data and length*2 stored in base slot +/// - Long (≥ 32 bytes): length*2+1 in base slot, data at keccak256(base_slot) +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct StorageBytes { + base_slot: U256, + _marker: PhantomData<()>, +} + +impl StorageBytes { + /// Creates a new bytes descriptor at the given base slot. + pub const fn new(base_slot: U256) -> Self { + Self { + base_slot, + _marker: PhantomData, + } + } + + /// Returns the storage slot for long byte's data. + fn data_slot(&self) -> U256 { + let hash = keccak256(self.base_slot.to_be_bytes::<32>()); + U256::from_be_bytes(hash.0) + } + + /// Reads the length and determines storage layout. + /// Returns (length, is_long_form). + fn read_length(&self, sdk: &S) -> (usize, bool) { + let word = sdk.sload(self.base_slot); + let last_byte = word.0[31]; + + if last_byte & 1 == 0 { + // Short form: length = last_byte / 2 + ((last_byte / 2) as usize, false) + } else { + // Long form: length = (word / 2) + let word_value = U256::from_be_bytes(word.0); + let len = word_value / U256::from(2); + (len.try_into().unwrap_or(0), true) + } + } + + /// Writes the length to storage. + fn write_length(&self, sdk: &mut S, len: usize) { + if len < 32 { + // Short form: store length * 2 in the last byte + let mut word = sdk.sload(self.base_slot); + word.0[31] = (len * 2) as u8; + sdk.sstore(self.base_slot, word); + } else { + // Long form: store length * 2 + 1 + let len_encoded = U256::from(len * 2 + 1); + sdk.sstore(self.base_slot, B256::from(len_encoded.to_be_bytes::<32>())); + } + } +} + +impl StorageDescriptor for StorageBytes { + fn new(slot: U256, offset: u8) -> Self { + debug_assert_eq!(offset, 0, "bytes always start at slot boundary"); + Self { + base_slot: slot, + _marker: PhantomData, + } + } + + fn slot(&self) -> U256 { + self.base_slot + } + + fn offset(&self) -> u8 { + 0 + } +} + +// --- 2. BytesAccess Implementation --- + +impl BytesAccess for StorageBytes { + fn len(&self, sdk: &S) -> usize { + self.read_length(sdk).0 + } + + fn is_empty(&self, sdk: &S) -> bool { + self.len(sdk) == 0 + } + + fn get(&self, sdk: &S, index: usize) -> u8 { + let (len, is_long) = self.read_length(sdk); + assert!(index < len, "bytes index out of bounds"); + + if !is_long { + // Short form: read from the base slot + sdk.sload(self.base_slot).0[index] + } else { + // Long form: read from data slots + let slot = self.data_slot() + U256::from(index / 32); + sdk.sload(slot).0[index % 32] + } + } + + fn push(&self, sdk: &mut S, byte: u8) { + let (old_len, _) = self.read_length(sdk); + let new_len = old_len + 1; + + if old_len < 31 { + // Still short after adding a byte + let mut word = sdk.sload(self.base_slot); + word.0[old_len] = byte; + word.0[31] = (new_len * 2) as u8; + sdk.sstore(self.base_slot, word); + } else if old_len == 31 { + // Converting from short to long form + let mut word = sdk.sload(self.base_slot); + word.0[31] = byte; + + // Copy data to long storage + sdk.sstore(self.data_slot(), word); + + // Update length to long form + self.write_length(sdk, new_len); + } else { + // Already long form + let slot = self.data_slot() + U256::from(old_len / 32); + let mut word = sdk.sload(slot); + word.0[old_len % 32] = byte; + sdk.sstore(slot, word); + + self.write_length(sdk, new_len); + } + } + + fn pop(&self, sdk: &mut S) -> Option { + let (len, is_long) = self.read_length(sdk); + if len == 0 { + return None; + } + + let index = len - 1; + let byte = self.get(sdk, index); + + if len == 32 && is_long { + // Converting from long to short form + let mut word = sdk.sload(self.data_slot()); + word.0[31] = (index * 2) as u8; + sdk.sstore(self.base_slot, word); + + // Clear the data slot + sdk.sstore(self.data_slot(), B256::ZERO); + } else if !is_long { + // Short form + let mut word = sdk.sload(self.base_slot); + word.0[index] = 0; + word.0[31] = (index * 2) as u8; + sdk.sstore(self.base_slot, word); + } else { + // Long form - just update length + self.write_length(sdk, index); + + // Clear the slot if it's now empty + if index % 32 == 0 && index > 0 { + let slot = self.data_slot() + U256::from(index / 32); + sdk.sstore(slot, B256::ZERO); + } + } + + Some(byte) + } + + fn load(&self, sdk: &S) -> Vec { + let (len, is_long) = self.read_length(sdk); + let mut result = Vec::with_capacity(len); + + if !is_long { + // Short form: read from the base slot + let word = sdk.sload(self.base_slot); + result.extend_from_slice(&word.0[..len]); + } else { + // Long form: read from data slots + let data_base = self.data_slot(); + for chunk_start in (0..len).step_by(32) { + let slot = data_base + U256::from(chunk_start / 32); + let word = sdk.sload(slot); + let chunk_end = (chunk_start + 32).min(len); + result.extend_from_slice(&word.0[..(chunk_end - chunk_start)]); + } + } + + result + } + + fn store(&self, sdk: &mut S, bytes: &[u8]) { + let new_len = bytes.len(); + let (old_len, was_long) = self.read_length(sdk); + + // Clear old long-form data if necessary + if was_long && old_len > 31 { + let data_base = self.data_slot(); + for i in 0..old_len.div_ceil(32) { + sdk.sstore(data_base + U256::from(i), B256::ZERO); + } + } + + if new_len < 32 { + // Store in short form + let mut word = B256::ZERO; + word.0[..new_len].copy_from_slice(bytes); + word.0[31] = (new_len * 2) as u8; + sdk.sstore(self.base_slot, word); + } else { + // Store in long form + let data_base = self.data_slot(); + + // Write data in 32-byte chunks + for (i, chunk) in bytes.chunks(32).enumerate() { + let mut word = B256::ZERO; + word.0[..chunk.len()].copy_from_slice(chunk); + sdk.sstore(data_base + U256::from(i), word); + } + + // Write length + self.write_length(sdk, new_len); + } + } + + fn clear(&self, sdk: &mut S) { + let (len, is_long) = self.read_length(sdk); + + // Clear data slots if in long form + if is_long && len > 31 { + let data_base = self.data_slot(); + for i in 0..len.div_ceil(32) { + sdk.sstore(data_base + U256::from(i), B256::ZERO); + } + } + + // Clear base slot (sets length to 0) + sdk.sstore(self.base_slot, B256::ZERO); + } +} + +// --- 3. StorageLayout Implementation --- + +impl StorageLayout for StorageBytes { + type Descriptor = StorageBytes; + type Accessor = Self; + + const REQUIRED_SLOTS: usize = 1; + const ENCODED_SIZE: usize = 32; + + fn access(descriptor: Self::Descriptor) -> Self::Accessor { + descriptor + } +} + +// --- 4. StorageString - Simple wrapper around Bytes --- + +/// A descriptor for UTF-8 strings in storage. +/// Simple wrapper around Bytes with string-specific convenience methods. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct StorageString { + bytes: StorageBytes, +} + +impl StorageString { + /// Creates a new string descriptor at the given base slot. + pub const fn new(base_slot: U256) -> Self { + Self { + bytes: StorageBytes::new(base_slot), + } + } + + /// Loads the string from storage. + pub fn get_string(&self, sdk: &S) -> String { + let bytes = self.bytes.load(sdk); + String::from_utf8_lossy(&bytes).into_owned() + } + + /// Stores a string to storage. + pub fn set_string(&self, sdk: &mut S, value: &str) { + self.bytes.store(sdk, value.as_bytes()); + } + + /// Appends a string slice. + pub fn push_str(&self, sdk: &mut S, string: &str) { + for byte in string.bytes() { + self.bytes.push(sdk, byte); + } + } +} + +impl StorageDescriptor for StorageString { + fn new(slot: U256, offset: u8) -> Self { + debug_assert_eq!(offset, 0, "strings always start at slot boundary"); + Self { + bytes: StorageBytes::new(slot), + } + } + + fn slot(&self) -> U256 { + self.bytes.slot() + } + + fn offset(&self) -> u8 { + self.bytes.offset() + } +} + +// Delegate BytesAccess to inner Bytes +impl BytesAccess for StorageString { + fn len(&self, sdk: &S) -> usize { + self.bytes.len(sdk) + } + + fn is_empty(&self, sdk: &S) -> bool { + self.bytes.is_empty(sdk) + } + + fn get(&self, sdk: &S, index: usize) -> u8 { + self.bytes.get(sdk, index) + } + + fn push(&self, sdk: &mut S, byte: u8) { + self.bytes.push(sdk, byte); + } + + fn pop(&self, sdk: &mut S) -> Option { + self.bytes.pop(sdk) + } + + fn load(&self, sdk: &S) -> Vec { + self.bytes.load(sdk) + } + + fn store(&self, sdk: &mut S, bytes: &[u8]) { + self.bytes.store(sdk, bytes); + } + + fn clear(&self, sdk: &mut S) { + self.bytes.clear(sdk); + } +} + +impl StorageLayout for StorageString { + type Descriptor = StorageString; + type Accessor = Self; + + const REQUIRED_SLOTS: usize = 1; + const ENCODED_SIZE: usize = 32; + + fn access(descriptor: Self::Descriptor) -> Self::Accessor { + descriptor + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::mock::MockStorage; + + #[test] + fn test_short_bytes_storage_layout() { + let mut sdk = MockStorage::new(); + let bytes = StorageBytes::new(U256::from(100)); + + // Test empty state + assert_eq!(sdk.get_slot_hex(U256::from(100)), "0".repeat(64)); + + // Add 3 bytes + bytes.push(&mut sdk, 0x11); + bytes.push(&mut sdk, 0x22); + bytes.push(&mut sdk, 0x33); + + // Verify storage: data at the beginning, length*2 at the end + let expected = "1122330000000000000000000000000000000000000000000000000000000006"; + assert_eq!(sdk.get_slot_hex(U256::from(100)), expected); + + // Verify API + assert_eq!(bytes.len(&sdk), 3); + assert_eq!(bytes.load(&sdk), vec![0x11, 0x22, 0x33]); + } + + #[test] + fn test_long_bytes_storage_layout() { + let mut sdk = MockStorage::new(); + let bytes = StorageBytes::new(U256::from(200)); + + // Push exactly 32 bytes + let data: Vec = (0..32).collect(); + for &b in &data { + bytes.push(&mut sdk, b); + } + + // Verify the base slot contains length*2+1 = 65 = 0x41 + let expected_base = "0000000000000000000000000000000000000000000000000000000000000041"; + assert_eq!(sdk.get_slot_hex(U256::from(200)), expected_base); + + // Verify data is at keccak256(base_slot) + let data_slot = { + let hash = keccak256(U256::from(200).to_be_bytes::<32>()); + U256::from_be_bytes(hash.0) + }; + + let mut expected_data = String::new(); + for i in 0..32 { + expected_data.push_str(&format!("{i:02x}")); + } + assert_eq!(sdk.get_slot_hex(data_slot), expected_data); + } + + #[test] + fn test_bytes_transition_short_to_long() { + let mut sdk = MockStorage::new(); + let bytes = StorageBytes::new(U256::from(300)); + + // Fill to 31 bytes (maximum short form) + for i in 0..31 { + bytes.push(&mut sdk, i as u8); + } + + // Verify short form storage + let mut expected = String::new(); + for i in 0..31 { + expected.push_str(&format!("{i:02x}")); + } + expected.push_str("3e"); // 31*2 = 62 = 0x3e + assert_eq!(sdk.get_slot_hex(U256::from(300)), expected); + + // Push one more byte to trigger the long form + bytes.push(&mut sdk, 31); + + // Verify long form: base slot has length*2+1 = 65 = 0x41 + assert_eq!( + sdk.get_slot_hex(U256::from(300)), + "0000000000000000000000000000000000000000000000000000000000000041" + ); + + // Verify data moved to keccak256(base_slot) + let data_slot = { + let hash = keccak256(U256::from(300).to_be_bytes::<32>()); + U256::from_be_bytes(hash.0) + }; + + let mut expected_data = String::new(); + for i in 0..32 { + expected_data.push_str(&format!("{i:02x}")); + } + assert_eq!(sdk.get_slot_hex(data_slot), expected_data); + } + + #[test] + fn test_bytes_transition_long_to_short() { + let mut sdk = MockStorage::new(); + let bytes = StorageBytes::new(U256::from(400)); + + // Create long form (32 bytes) + for i in 0..32 { + bytes.push(&mut sdk, i as u8); + } + + let data_slot = { + let hash = keccak256(U256::from(400).to_be_bytes::<32>()); + U256::from_be_bytes(hash.0) + }; + + // Verify long form + assert_eq!( + sdk.get_slot_hex(U256::from(400)), + "0000000000000000000000000000000000000000000000000000000000000041" + ); + + // Pop one byte to trigger the short form + assert_eq!(bytes.pop(&mut sdk), Some(31)); + + // Verify short form restored + let mut expected = String::new(); + for i in 0..31 { + expected.push_str(&format!("{i:02x}")); + } + expected.push_str("3e"); // 31*2 = 62 = 0x3e + assert_eq!(sdk.get_slot_hex(U256::from(400)), expected); + + // Verify data slot cleared + assert_eq!(sdk.get_slot_hex(data_slot), "0".repeat(64)); + } + + #[test] + fn test_bytes_store_and_clear() { + let mut sdk = MockStorage::new(); + let bytes = StorageBytes::new(U256::from(500)); + + // Store long data + let data: Vec = (100..150).collect(); + bytes.store(&mut sdk, &data); + + // Verify length in base slot: 50*2+1 = 101 = 0x65 + assert_eq!( + sdk.get_slot_hex(U256::from(500)), + "0000000000000000000000000000000000000000000000000000000000000065" + ); + + // Verify data stored correctly + assert_eq!(bytes.load(&sdk), data); + + // Store short data (should clear old slots) + let short_data = vec![0xAA, 0xBB, 0xCC]; + bytes.store(&mut sdk, &short_data); + + // Verify short form + assert_eq!( + sdk.get_slot_hex(U256::from(500)), + "aabbcc0000000000000000000000000000000000000000000000000000000006" + ); + + // Clear all + bytes.clear(&mut sdk); + assert_eq!(sdk.get_slot_hex(U256::from(500)), "0".repeat(64)); + assert!(bytes.is_empty(&sdk)); + } + + #[test] + fn test_string_storage() { + let mut sdk = MockStorage::new(); + let string = StorageString::new(U256::from(600)); + + // Test short string + string.set_string(&mut sdk, "Hello"); + assert_eq!(string.get_string(&sdk), "Hello"); + + // Verify hex storage + let hex = sdk.get_slot_hex(U256::from(600)); + assert_eq!(&hex[0..10], "48656c6c6f"); // "Hello" in hex + assert_eq!(&hex[62..64], "0a"); // length*2 = 10 + + // Test long string + let long_str = "This is a very long string that will use long form storage!"; + string.set_string(&mut sdk, long_str); + assert_eq!(string.get_string(&sdk), long_str); + + // Verify base slot has length encoded + let len_encoded = long_str.len() * 2 + 1; + assert_eq!(sdk.get_slot(U256::from(600)), U256::from(len_encoded)); + } + + #[test] + #[should_panic(expected = "bytes index out of bounds")] + fn test_bytes_bounds_check() { + let sdk = MockStorage::new(); + let bytes = StorageBytes::new(U256::from(800)); + bytes.get(&sdk, 0); // Should panic - empty bytes + } + + #[test] + fn test_bytes_multiple_slots() { + let mut sdk = MockStorage::new(); + let bytes = StorageBytes::new(U256::from(900)); + + // Store 100 bytes (requires 4 slots) + let data: Vec = (0..100).collect(); + bytes.store(&mut sdk, &data); + + // Verify all data stored and retrieved correctly + assert_eq!(bytes.load(&sdk), data); + assert_eq!(bytes.len(&sdk), 100); + + // Verify individual bytes + for i in 0..100 { + assert_eq!(bytes.get(&sdk, i), i as u8); + } + } +} diff --git a/crates/sdk/src/storage/composite.rs b/crates/sdk/src/storage/composite.rs new file mode 100644 index 000000000..374f376e8 --- /dev/null +++ b/crates/sdk/src/storage/composite.rs @@ -0,0 +1,70 @@ +use crate::{ + storage::{StorageDescriptor, StorageLayout}, + U256, +}; +use core::marker::PhantomData; + +// TODO(d1r1): double-check if we actually need custom Clone, Copy impls here +#[derive(Debug, PartialEq, Eq)] +pub struct Composite { + base_slot: U256, + _phantom: PhantomData, +} + +impl Clone for Composite { + fn clone(&self) -> Self { + Self { + base_slot: self.base_slot, + _phantom: PhantomData, + } + } +} +impl Copy for Composite {} + +impl Composite { + pub const fn new(base_slot: U256) -> Self { + Self { + base_slot, + _phantom: PhantomData, + } + } +} + +impl StorageDescriptor for Composite { + fn new(slot: U256, offset: u8) -> Self { + debug_assert_eq!(offset, 0, "Composite types always start at slot boundary"); + Self::new(slot) + } + + fn slot(&self) -> U256 { + self.base_slot + } + + fn offset(&self) -> u8 { + 0 + } +} + +// ===== CompositeStorage trait ===== + +/// Trait for composite storage types that can be wrapped in Composite +pub trait CompositeStorage: Sized { + /// Number of slots required for this composite type + const REQUIRED_SLOTS: usize; + + /// Create instance from base slot + fn from_slot(base_slot: U256) -> Self; +} + +// Implement StorageLayout for Composite +impl StorageLayout for Composite { + type Descriptor = Self; + type Accessor = T; + + const REQUIRED_SLOTS: usize = T::REQUIRED_SLOTS; + const ENCODED_SIZE: usize = T::REQUIRED_SLOTS * 32; + + fn access(descriptor: Self::Descriptor) -> Self::Accessor { + T::from_slot(descriptor.base_slot) + } +} diff --git a/crates/sdk/src/storage/map.rs b/crates/sdk/src/storage/map.rs new file mode 100644 index 000000000..3d61b54e7 --- /dev/null +++ b/crates/sdk/src/storage/map.rs @@ -0,0 +1,494 @@ +use crate::storage::PrimitiveCodec; +use crate::{ + keccak256, + storage::{MapAccess, MapKey, StorageDescriptor, StorageLayout}, + U256, +}; +use alloc::{string::String, vec::Vec}; +use core::marker::PhantomData; +// --- 1. Map Descriptor --- + +/// A descriptor for a storage map (mapping in Solidity). +/// Maps don't store data directly at the base slot - it's used only for slot calculation. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct StorageMap { + base_slot: U256, + _marker: PhantomData<(K, V)>, +} + +impl StorageMap { + /// Creates a new map descriptor at the given base slot. + pub const fn new(base_slot: U256) -> Self { + Self { + base_slot, + _marker: PhantomData, + } + } +} + +impl StorageDescriptor for StorageMap { + fn new(slot: U256, _offset: u8) -> Self { + Self::new(slot) + } + + fn slot(&self) -> U256 { + self.base_slot + } + + fn offset(&self) -> u8 { + 0 + } +} + +// --- 2. MapAccess Implementation --- + +impl MapAccess for StorageMap +where + V::Descriptor: StorageDescriptor, +{ + fn entry(&self, key: K) -> V::Accessor { + let value_slot = key.compute_slot(self.base_slot); + + // For primitive types smaller than 32 bytes, calculate proper offset + let offset = if V::ENCODED_SIZE < 32 { + (32 - V::ENCODED_SIZE) as u8 + } else { + 0 + }; + + let value_descriptor = V::Descriptor::new(value_slot, offset); + + V::access(value_descriptor) + } +} + +// --- 3. StorageLayout Implementation --- + +impl StorageLayout for StorageMap +where + V::Descriptor: StorageDescriptor, +{ + type Descriptor = StorageMap; + type Accessor = Self; + + // Maps only reserve the base slot for computation + const REQUIRED_SLOTS: usize = 1; + const ENCODED_SIZE: usize = 32; + + fn access(descriptor: Self::Descriptor) -> Self::Accessor { + descriptor + } +} + +// --- 4. MapKey Implementations --- + +impl MapKey for T +where + T: PrimitiveCodec, +{ + fn compute_slot(&self, root_slot: U256) -> U256 { + let mut key_bytes = [0u8; 32]; + let encoded_size = T::ENCODED_SIZE; + + if encoded_size <= 32 { + let offset = 32 - encoded_size; + self.encode_into(&mut key_bytes[offset..]); + } + + let mut data = [0u8; 64]; + data[0..32].copy_from_slice(&key_bytes); + data[32..64].copy_from_slice(&root_slot.to_be_bytes::<32>()); + + let hash = keccak256(data); + U256::from_be_bytes(hash.0) + } +} + +impl MapKey for &[u8] { + fn compute_slot(&self, root_slot: U256) -> U256 { + // For dynamic keys: keccak256(key || slot) + let mut data = Vec::with_capacity(self.len() + 32); + data.extend_from_slice(self); + data.extend_from_slice(&root_slot.to_be_bytes::<32>()); + + let hash = keccak256(&data); + U256::from_be_bytes(hash.0) + } +} + +impl MapKey for Vec { + fn compute_slot(&self, root_slot: U256) -> U256 { + self.as_slice().compute_slot(root_slot) + } +} + +impl MapKey for &str { + fn compute_slot(&self, root_slot: U256) -> U256 { + self.as_bytes().compute_slot(root_slot) + } +} + +impl MapKey for String { + fn compute_slot(&self, root_slot: U256) -> U256 { + self.as_bytes().compute_slot(root_slot) + } +} + +// Implement for signed integers +impl MapKey for i64 { + fn compute_slot(&self, root_slot: U256) -> U256 { + // Two's complement representation + let value = U256::from(*self as u64); + value.compute_slot(root_slot) + } +} + +impl MapKey for i32 { + fn compute_slot(&self, root_slot: U256) -> U256 { + let value = U256::from(*self as u32); + value.compute_slot(root_slot) + } +} + +impl MapKey for i16 { + fn compute_slot(&self, root_slot: U256) -> U256 { + let value = U256::from(*self as u16); + value.compute_slot(root_slot) + } +} + +impl MapKey for i8 { + fn compute_slot(&self, root_slot: U256) -> U256 { + let value = U256::from(*self as u8); + value.compute_slot(root_slot) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::{ + array::StorageArray, mock::MockStorage, primitive::StoragePrimitive, ArrayAccess, + PrimitiveAccess, + }; + + #[test] + fn test_map_basic_operations() { + let mut sdk = MockStorage::new(); + let map = StorageMap::>::new(U256::from(100)); + + // Set values for different keys + map.entry(U256::from(1)).set(&mut sdk, U256::from(111)); + map.entry(U256::from(2)).set(&mut sdk, U256::from(222)); + map.entry(U256::from(42)).set(&mut sdk, U256::from(424242)); + + // Get values back + assert_eq!(map.entry(U256::from(1)).get(&sdk), U256::from(111)); + assert_eq!(map.entry(U256::from(2)).get(&sdk), U256::from(222)); + assert_eq!(map.entry(U256::from(42)).get(&sdk), U256::from(424242)); + + // Non-existent key returns zero + assert_eq!(map.entry(U256::from(999)).get(&sdk), U256::ZERO); + } + + #[test] + fn test_map_slot_calculation() { + // Test that slot calculation matches Solidity's keccak256(key || slot) + let key = U256::from(7); + let expected_slot = { + let mut data = [0u8; 64]; + data[0..32].copy_from_slice(&key.to_be_bytes::<32>()); + data[32..64].copy_from_slice(&U256::from(5).to_be_bytes::<32>()); + let hash = keccak256(data); + U256::from_be_bytes(hash.0) + }; + + assert_eq!(key.compute_slot(U256::from(5)), expected_slot); + } + + #[test] + fn test_map_with_various_key_types() { + let mut sdk = MockStorage::new(); + + // Bool keys + let bool_map = StorageMap::>::new(U256::from(200)); + bool_map.entry(true).set(&mut sdk, U256::from(100)); + bool_map.entry(false).set(&mut sdk, U256::from(200)); + assert_eq!(bool_map.entry(true).get(&sdk), U256::from(100)); + assert_eq!(bool_map.entry(false).get(&sdk), U256::from(200)); + + // String keys + let string_map = StorageMap::<&str, StoragePrimitive>::new(U256::from(300)); + string_map.entry("alice").set(&mut sdk, U256::from(1000)); + string_map.entry("bob").set(&mut sdk, U256::from(2000)); + assert_eq!(string_map.entry("alice").get(&sdk), U256::from(1000)); + assert_eq!(string_map.entry("bob").get(&sdk), U256::from(2000)); + + // u64 keys + let u64_map = StorageMap::>::new(U256::from(400)); + u64_map.entry(12345u64).set(&mut sdk, U256::from(999)); + + assert_eq!(u64_map.entry(12345u64).get(&sdk), U256::from(999)); + } + + #[test] + fn test_nested_maps() { + let mut sdk = MockStorage::new(); + // Map>> + let map = + StorageMap::>>::new(U256::from(500)); + + // Set nested values + map.entry(U256::from(1)) + .entry(U256::from(10)) + .set(&mut sdk, U256::from(110)); + + map.entry(U256::from(1)) + .entry(U256::from(20)) + .set(&mut sdk, U256::from(120)); + + map.entry(U256::from(2)) + .entry(U256::from(10)) + .set(&mut sdk, U256::from(210)); + + // Get nested values + assert_eq!( + map.entry(U256::from(1)).entry(U256::from(10)).get(&sdk), + U256::from(110) + ); + assert_eq!( + map.entry(U256::from(1)).entry(U256::from(20)).get(&sdk), + U256::from(120) + ); + assert_eq!( + map.entry(U256::from(2)).entry(U256::from(10)).get(&sdk), + U256::from(210) + ); + } + + #[test] + fn test_map_with_arrays_as_values() { + let mut sdk = MockStorage::new(); + // Map, 3>> + let map = StorageMap::, 3>>::new(U256::from(600)); + + // Set array values for key 1 + let array1 = map.entry(U256::from(1)); + array1.at(0).set(&mut sdk, 100u64); + array1.at(1).set(&mut sdk, 200u64); + array1.at(2).set(&mut sdk, 300u64); + + // Set array values for key 2 + let array2 = map.entry(U256::from(2)); + array2.at(0).set(&mut sdk, 400u64); + array2.at(1).set(&mut sdk, 500u64); + array2.at(2).set(&mut sdk, 600u64); + + // Verify values + assert_eq!(map.entry(U256::from(1)).at(0).get(&sdk), 100u64); + assert_eq!(map.entry(U256::from(1)).at(1).get(&sdk), 200u64); + assert_eq!(map.entry(U256::from(1)).at(2).get(&sdk), 300u64); + + assert_eq!(map.entry(U256::from(2)).at(0).get(&sdk), 400u64); + assert_eq!(map.entry(U256::from(2)).at(1).get(&sdk), 500u64); + assert_eq!(map.entry(U256::from(2)).at(2).get(&sdk), 600u64); + } + + #[test] + fn test_map_overwrites() { + let mut sdk = MockStorage::new(); + let map = StorageMap::>::new(U256::from(700)); + + // Set initial value + map.entry(U256::from(42)).set(&mut sdk, U256::from(100)); + assert_eq!(map.entry(U256::from(42)).get(&sdk), U256::from(100)); + + // Overwrite + map.entry(U256::from(42)).set(&mut sdk, U256::from(200)); + assert_eq!(map.entry(U256::from(42)).get(&sdk), U256::from(200)); + } + + #[test] + fn test_map_storage_isolation() { + let mut sdk = MockStorage::new(); + + // Two maps at different slots + let map1 = StorageMap::>::new(U256::from(800)); + let map2 = StorageMap::>::new(U256::from(801)); + + // Same key, different values + map1.entry(U256::from(1)).set(&mut sdk, U256::from(111)); + map2.entry(U256::from(1)).set(&mut sdk, U256::from(222)); + + // Values should be isolated + assert_eq!(map1.entry(U256::from(1)).get(&sdk), U256::from(111)); + assert_eq!(map2.entry(U256::from(1)).get(&sdk), U256::from(222)); + } + + #[test] + fn test_map_storage_layout() { + let mut sdk = MockStorage::new(); + let map = StorageMap::>::new(U256::from(5)); + + // Set value for key = 7 + let key = U256::from(7); + let value = U256::from(0x123456); + map.entry(key).set(&mut sdk, value); + + // Calculate expected slot: keccak256(key || base_slot) + let mut data = [0u8; 64]; + data[0..32].copy_from_slice(&key.to_be_bytes::<32>()); + data[32..64].copy_from_slice(&U256::from(5).to_be_bytes::<32>()); + let expected_slot = U256::from_be_bytes(keccak256(data).0); + + // Verify value is stored at the correct slot + assert_eq!(sdk.get_slot(expected_slot), value); + + // Verify the base slot remains empty (maps don't store data there) + assert_eq!(sdk.get_slot(U256::from(5)), U256::ZERO); + } + + #[test] + fn test_map_with_packed_values() { + let mut sdk = MockStorage::new(); + let map = StorageMap::>::new(U256::from(10)); + + // Set u64 value for key = 1 + map.entry(U256::from(1)) + .set(&mut sdk, 0xDEADBEEFCAFEBABEu64); + + // Calculate slot + let mut data = [0u8; 64]; + data[0..32].copy_from_slice(&U256::from(1).to_be_bytes::<32>()); + data[32..64].copy_from_slice(&U256::from(10).to_be_bytes::<32>()); + let slot = U256::from_be_bytes(keccak256(data).0); + + // u64 should be stored at the rightmost 8 bytes (offset 24) + let stored = sdk.get_slot_hex(slot); + assert_eq!(&stored[48..], "deadbeefcafebabe"); // Last 16 hex chars = 8 bytes + } + + #[test] + fn test_map_key_types_storage() { + let mut sdk = MockStorage::new(); + + // Test bool key storage + let bool_map = StorageMap::>::new(U256::from(20)); + bool_map.entry(true).set(&mut sdk, U256::from(100)); + + // Calculate slot for true (encoded as 1) + let mut data = [0u8; 64]; + data[0..32].copy_from_slice(&U256::from(1).to_be_bytes::<32>()); + data[32..64].copy_from_slice(&U256::from(20).to_be_bytes::<32>()); + let slot_true = U256::from_be_bytes(keccak256(data).0); + assert_eq!(sdk.get_slot(slot_true), U256::from(100)); + + // Test string key storage + let string_map = StorageMap::<&str, StoragePrimitive>::new(U256::from(30)); + string_map.entry("test").set(&mut sdk, U256::from(999)); + + // Calculate slot for "test" + let mut str_data = Vec::new(); + str_data.extend_from_slice(b"test"); + str_data.extend_from_slice(&U256::from(30).to_be_bytes::<32>()); + let slot_test = U256::from_be_bytes(keccak256(&str_data).0); + assert_eq!(sdk.get_slot(slot_test), U256::from(999)); + } + + #[test] + fn test_nested_maps_storage() { + let mut sdk = MockStorage::new(); + let map = StorageMap::>>::new(U256::from(40)); + + // Set map[1][2] = 100 + map.entry(U256::from(1)) + .entry(U256::from(2)) + .set(&mut sdk, U256::from(100)); + + // Calculate first level slot: keccak256(1 || 40) + let mut data1 = [0u8; 64]; + data1[0..32].copy_from_slice(&U256::from(1).to_be_bytes::<32>()); + data1[32..64].copy_from_slice(&U256::from(40).to_be_bytes::<32>()); + let slot1 = U256::from_be_bytes(keccak256(data1).0); + + // Calculate second level slot: keccak256(2 || slot1) + let mut data2 = [0u8; 64]; + data2[0..32].copy_from_slice(&U256::from(2).to_be_bytes::<32>()); + data2[32..64].copy_from_slice(&slot1.to_be_bytes::<32>()); + let slot2 = U256::from_be_bytes(keccak256(data2).0); + + // Verify value is at the correct nested slot + assert_eq!(sdk.get_slot(slot2), U256::from(100)); + } + + #[test] + fn test_map_with_array_values_storage() { + let mut sdk = MockStorage::new(); + let map = StorageMap::, 3>>::new(U256::from(50)); + + // Set array values for key = 5 + let array = map.entry(U256::from(5)); + array.at(0).set(&mut sdk, 0x1111u64); + array.at(1).set(&mut sdk, 0x2222u64); + array.at(2).set(&mut sdk, 0x3333u64); + + // Calculate base slot for the array + let mut data = [0u8; 64]; + data[0..32].copy_from_slice(&U256::from(5).to_be_bytes::<32>()); + data[32..64].copy_from_slice(&U256::from(50).to_be_bytes::<32>()); + let array_slot = U256::from_be_bytes(keccak256(data).0); + + // All 3 u64 values should be packed in one slot + // Layout: [empty(8)] [elem2(8)] [elem1(8)] [elem0(8)] + let stored = sdk.get_slot_hex(array_slot); + let expected = "0000000000000000000000000000333300000000000022220000000000001111"; + + assert_eq!(&stored, expected); + } + + #[test] + fn test_map_isolation() { + let mut sdk = MockStorage::new(); + + // Two maps at different slots + let map1 = StorageMap::>::new(U256::from(60)); + let map2 = StorageMap::>::new(U256::from(61)); + + // Same key, different values + map1.entry(U256::from(1)).set(&mut sdk, U256::from(111)); + map2.entry(U256::from(1)).set(&mut sdk, U256::from(222)); + + // Calculate slots + let mut data1 = [0u8; 64]; + data1[0..32].copy_from_slice(&U256::from(1).to_be_bytes::<32>()); + data1[32..64].copy_from_slice(&U256::from(60).to_be_bytes::<32>()); + let slot1 = U256::from_be_bytes(keccak256(data1).0); + + let mut data2 = [0u8; 64]; + data2[0..32].copy_from_slice(&U256::from(1).to_be_bytes::<32>()); + data2[32..64].copy_from_slice(&U256::from(61).to_be_bytes::<32>()); + let slot2 = U256::from_be_bytes(keccak256(data2).0); + + // Verify slots are different and contain correct values + assert_ne!(slot1, slot2); + assert_eq!(sdk.get_slot(slot1), U256::from(111)); + assert_eq!(sdk.get_slot(slot2), U256::from(222)); + } + + #[test] + fn test_map_zero_key() { + let mut sdk = MockStorage::new(); + let map = StorageMap::>::new(U256::from(70)); + + // Test with key = 0 + map.entry(U256::ZERO).set(&mut sdk, U256::from(0xABCDEF)); + + // Calculate slot for key = 0 + let mut data = [0u8; 64]; + data[0..32].copy_from_slice(&U256::ZERO.to_be_bytes::<32>()); + data[32..64].copy_from_slice(&U256::from(70).to_be_bytes::<32>()); + let slot = U256::from_be_bytes(keccak256(data).0); + + assert_eq!(sdk.get_slot(slot), U256::from(0xABCDEF)); + } +} diff --git a/crates/sdk/src/storage/mock.rs b/crates/sdk/src/storage/mock.rs new file mode 100644 index 000000000..0660846c8 --- /dev/null +++ b/crates/sdk/src/storage/mock.rs @@ -0,0 +1,70 @@ +#![allow(dead_code)] +use alloc::string::String; +use alloc::vec::Vec; +use fluentbase_types::{hex, syscall::SyscallResult, ExitCode, StorageAPI, U256}; +use hashbrown::HashMap; + +/// A mock implementation of `StorageAPI` that simulates contract storage +/// using an in-memory HashMap. It is `Clone`-able to easily create +/// separate instances for different testing phases or checks. +#[derive(Clone, Default, Debug)] +pub struct MockStorage { + pub storage: HashMap, +} + +impl MockStorage { + /// Creates a new, empty `MockSDK`. + pub fn new() -> Self { + Default::default() + } + + /// Retrieves the raw `U256` value from a specific storage slot. + /// Returns `U256::ZERO` if the slot has not been written to. + pub fn get_slot(&self, slot: impl Into) -> U256 { + self.storage.get(&slot.into()).copied().unwrap_or_default() + } + + /// A convenience helper to get the raw slot content as a hex string. + /// Useful for snapshot testing of packed data. + pub fn get_slot_hex(&self, slot: impl Into) -> String { + hex::encode(self.get_slot(slot).to_be_bytes::<32>()) + } + + /// A setup helper to directly initialize storage with raw data from a hex string. + /// Panics if the hex string is invalid. + pub fn init_slot(&mut self, slot: impl Into, hex_data: &str) -> &mut Self { + let slot = slot.into(); + let stripped = hex_data.strip_prefix("0x").unwrap_or(hex_data); + let bytes = hex::decode(stripped).expect("invalid hex string in init_slot"); + let value = + U256::from_be_bytes::<32>(bytes.try_into().expect("hex string must be 32 bytes long")); + self.storage.insert(slot, value); + self + } + + /// Returns all slots sorted by key + pub fn sorted_slots(&self) -> Vec<(U256, U256)> { + let mut slots: Vec<(U256, U256)> = self.storage.iter().map(|(k, v)| (*k, *v)).collect(); + slots.sort_by_key(|(slot, _)| *slot); + slots + } +} + +// --- StorageAPI Implementation --- + +impl StorageAPI for MockStorage { + fn write_storage(&mut self, slot: U256, value: U256) -> SyscallResult<()> { + if value == U256::ZERO { + // Mimic EVM behavior: setting to zero is equivalent to deleting. + self.storage.remove(&slot); + } else { + self.storage.insert(slot, value); + } + SyscallResult::new((), 0, 0, ExitCode::Ok) + } + + fn storage(&self, slot: &U256) -> SyscallResult { + let value = self.get_slot(*slot); + SyscallResult::new(value, 0, 0, ExitCode::Ok) + } +} diff --git a/crates/sdk/src/storage/mod.rs b/crates/sdk/src/storage/mod.rs new file mode 100644 index 000000000..10d27a64e --- /dev/null +++ b/crates/sdk/src/storage/mod.rs @@ -0,0 +1,224 @@ +use crate::{B256, U256}; +use alloc::vec::Vec; +use fluentbase_types::StorageAPI; + +pub mod primitive; + +pub mod array; +pub mod bytes; +pub mod map; + +pub mod composite; +pub mod mock; +pub use mock::MockStorage; + +pub mod vec; + +/// The main trait that connects a Rust type to its storage layout and access API. +/// Any type that can be a field in a `#[fluent_storage]` struct must implement this. +pub trait StorageLayout: Sized { + /// A lightweight, `Copy`-able struct describing the storage location. + type Descriptor: StorageDescriptor; + + /// A temporary proxy object providing the interactive API for this type. + type Accessor; + + /// The number of contiguous slots required by this type's layout. + const REQUIRED_SLOTS: usize; + + const ENCODED_SIZE: usize; + + /// Creates an Accessor, the sole entry point for interacting with the stored value. + fn access(descriptor: Self::Descriptor) -> Self::Accessor; +} + +/// Trait that all storage descriptors must implement. +/// Provides a uniform interface for creating and accessing storage locations. +pub trait StorageDescriptor { + /// Creates a descriptor at the specified storage location. + /// For composite types (arrays, structs, maps), offset should be 0. + /// For packed primitive types, offset indicates position within the slot. + fn new(slot: U256, offset: u8) -> Self; + + /// Returns the base storage slot. + fn slot(&self) -> U256; + + /// Returns the offset within the slot (0 for non-packed types). + fn offset(&self) -> u8; +} + +/// A trait for types that have a fixed-size byte representation suitable for storage. +/// +/// This trait defines the low-level serialization and deserialization logic +/// for types that can be packed within a single 32-byte storage slot. +pub trait PrimitiveCodec: Sized { + /// The exact number of bytes this type occupies when encoded. Must be <= 32. + const ENCODED_SIZE: usize; + + /// Encodes the value into the provided byte slice. + /// + /// # Panics + /// if the length of `target` is not equal to `ENCODED_SIZE`. + fn encode_into(&self, target: &mut [u8]); + + /// Decodes a value from the provided byte slice. + /// + /// # Panics + /// Panics if the length of `bytes` is not equal to `ENCODED_SIZE`. + fn decode(bytes: &[u8]) -> Self; +} + +/// ---------------------------------- +/// Accessor traits +/// ---------------------------------- +/// Defines the API for a single, primitive value that implements `StorageCodec`. +pub trait PrimitiveAccess { + /// Reads the value from storage. + fn get(&self, sdk: &S) -> T; + + /// Writes a new value to storage. + fn set(&self, sdk: &mut S, value: T); +} + +/// Defines the API for a fixed-size array of `StorageLayout` elements. +pub trait ArrayAccess { + /// Returns an accessor for the element at the given index. + /// + /// # Panics + /// Panics if `index >= N`. + fn at(&self, index: usize) -> T::Accessor; +} +/// Defines the API for a key-value map where values are `StorageLayout` types. +pub trait MapAccess { + /// Returns an accessor for the value at the given key. + /// The accessor can be used to get or set the value. + fn entry(&self, key: K) -> V::Accessor; +} + +/// Defines the API for a dynamic vector of `StorageLayout` elements. +/// Dynamic vectors in Solidity store their length at the base slot, +/// and elements are stored starting at keccak256(base_slot). +pub trait VecAccess { + /// Returns the number of elements in the vector. + fn len(&self, sdk: &S) -> u64; + + /// Returns `true` if the vector is empty. + fn is_empty(&self, sdk: &S) -> bool { + self.len(sdk) == 0 + } + + /// Returns an accessor for the element at `index`. + /// # Panics + /// Panics if `index >= self.len()`. + fn at(&self, index: u64) -> T::Accessor; + + /// Appends a new element and returns an accessor to initialize it. + /// Updates the length and returns accessor to the new element. + fn push(&self, sdk: &mut S) -> T::Accessor; + + /// Removes the last element by decrementing the length. + /// Does not clear the storage slot (gas optimization). + fn pop(&self, sdk: &mut S); + + /// Clears the vector by setting length to 0. + /// Does not clear individual elements (gas optimization). + fn clear(&self, sdk: &mut S); +} +/// Defines the specialized API for dynamic byte arrays. +/// Dynamic byte arrays in Solidity use optimized storage: +/// - Short (< 32 bytes): data and length stored inline +/// - Long (≥ 32 bytes): length at base slot, data at keccak256(base_slot) +pub trait BytesAccess { + /// Returns the number of bytes. + fn len(&self, sdk: &S) -> usize; + + /// Returns `true` if the array is empty. + fn is_empty(&self, sdk: &S) -> bool { + self.len(sdk) == 0 + } + + /// Reads a single byte at `index`. + /// # Panics + /// Panics if `index >= self.len()`. + fn get(&self, sdk: &S, index: usize) -> u8; + + /// Appends a byte. + fn push(&self, sdk: &mut S, byte: u8); + + /// Removes and returns the last byte, or `None` if empty. + fn pop(&self, sdk: &mut S) -> Option; + + /// Reads all bytes into a `Vec`. + /// Use with caution due to gas costs for large arrays. + fn load(&self, sdk: &S) -> Vec; + + /// Overwrites the entire storage with new data. + /// Efficiently handles transition between short and long forms. + fn store(&self, sdk: &mut S, bytes: &[u8]); + + /// Clears all bytes by setting length to 0. + /// May clear storage slots for gas optimization. + fn clear(&self, sdk: &mut S); +} + +/// ---------------------------------- +/// Helper traits +/// ---------------------------------- +/// A trait for types that can be used as keys in a `Map`. +pub trait MapKey { + /// Computes the final storage slot for an item within a map. + fn compute_slot(&self, root_slot: U256) -> U256; +} + +/// An internal extension trait for `StorageAPI` providing low-level, +/// packed storage operations. Not intended for public use. +pub(crate) trait StorageOps: StorageAPI { + /// Reads a 32-byte word from storage, returning a B256. + /// Panics on syscall failure. + fn sload(&self, slot: U256) -> B256 { + self.storage(&slot).unwrap().into() + } + + /// Writes a 32-byte word to storage. + /// Panics on syscall failure. + fn sstore(&mut self, slot: U256, value: B256) { + self.write_storage(slot, value.into()).unwrap() + } + + /// Reads a value of type `T` that implements `StorageCodec` from a specific + /// location (slot + offset) within a storage word. + fn read_at(&self, slot: U256, offset: u8) -> T { + // offset is from the LEFT edge (start of byte array) + let start = offset as usize; + let end = start + T::ENCODED_SIZE; + + assert!(end <= 32, "read out of slot bounds"); + + let word = self.sload(slot); + T::decode(&word[start..end]) + } + + /// Writes a value of type `T` that implements `StorageCodec` to a specific + /// location (slot + offset) within a storage word. + fn write_at(&mut self, slot: U256, offset: u8, value: &T) { + // offset is from the LEFT edge (start of byte array) + let start = offset as usize; + let end = start + T::ENCODED_SIZE; + + assert!(end <= 32, "write out of slot bounds"); + + if T::ENCODED_SIZE == 32 && offset == 0 { + let mut word_bytes = [0u8; 32]; + value.encode_into(&mut word_bytes); + self.sstore(slot, B256::from(word_bytes)); + return; + } + + let mut word = self.sload(slot); + value.encode_into(&mut word.0[start..end]); + self.sstore(slot, word); + } +} + +/// Automatically implement `StorageOps` for any type that already implements `StorageAPI`. +impl StorageOps for S {} diff --git a/crates/sdk/src/storage/primitive.rs b/crates/sdk/src/storage/primitive.rs new file mode 100644 index 000000000..20fc7ea8d --- /dev/null +++ b/crates/sdk/src/storage/primitive.rs @@ -0,0 +1,299 @@ +use crate::storage::{ + PrimitiveAccess, PrimitiveCodec, StorageDescriptor, StorageLayout, StorageOps, +}; +use core::marker::PhantomData; +use fluentbase_types::{Address, StorageAPI, U256}; + +// --- 1. Descriptor --- + +/// A lightweight, copy-able descriptor that defines the storage location +/// of a single, packable value. +#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] +pub struct StoragePrimitive { + slot: U256, + offset: u8, + _marker: PhantomData, +} + +impl StoragePrimitive { + /// Creates a new descriptor for a primitive value at a specific location. + pub const fn new(slot: U256, offset: u8) -> Self { + Self { + slot, + offset, + _marker: PhantomData, + } + } +} + +impl StorageDescriptor for StoragePrimitive { + fn new(slot: U256, offset: u8) -> Self { + Self::new(slot, offset) + } + + fn slot(&self) -> U256 { + self.slot + } + + fn offset(&self) -> u8 { + self.offset + } +} + +// --- 2. Accessor --- + +/// A lightweight accessor that holds only the storage location descriptor. +/// SDK is passed to methods that need storage access. +pub struct PrimitiveAccessor { + descriptor: StoragePrimitive, +} + +impl PrimitiveAccessor { + /// Creates a new accessor. + pub(crate) fn new(descriptor: StoragePrimitive) -> Self { + Self { descriptor } + } +} + +// --- 3. API Implementation (impl PrimitiveAccess for PrimitiveAccessor) --- + +impl PrimitiveAccess for PrimitiveAccessor { + /// Reads the value from its storage slot, decodes it, and returns it. + fn get(&self, sdk: &S) -> T { + sdk.read_at(self.descriptor.slot, self.descriptor.offset) + } + + /// Encodes and writes a new value to its storage slot. + fn set(&self, sdk: &mut S, value: T) { + sdk.write_at(self.descriptor.slot, self.descriptor.offset, &value); + } +} + +// --- 4. Main Trait Implementation (impl StorageLayout for Primitive) --- + +impl StorageLayout for StoragePrimitive { + /// The descriptor for a `Primitive` is the struct itself. + type Descriptor = Self; + + /// The accessor for a `Primitive` is the `PrimitiveAccessor`. + type Accessor = PrimitiveAccessor; + + /// A single primitive value, even if small, reserves at least one full slot + /// in the storage layout calculation to avoid complex packing across fields. + /// Packing happens *within* the slot, managed by the `offset`. + const REQUIRED_SLOTS: usize = if T::ENCODED_SIZE == 32 { 1 } else { 0 }; + + const ENCODED_SIZE: usize = T::ENCODED_SIZE; + + /// The entry point to interacting with the primitive value. + fn access(descriptor: Self::Descriptor) -> Self::Accessor { + PrimitiveAccessor::new(descriptor) + } +} + +// --- 5. StorageCodec Implementations for Core Primitives --- + +impl PrimitiveCodec for U256 { + const ENCODED_SIZE: usize = 32; + + fn encode_into(&self, target: &mut [u8]) { + target.copy_from_slice(&self.to_be_bytes::<32>()); + } + + fn decode(bytes: &[u8]) -> Self { + U256::from_be_bytes::<32>(bytes.try_into().unwrap()) + } +} + +impl PrimitiveCodec for Address { + const ENCODED_SIZE: usize = 20; + + fn encode_into(&self, target: &mut [u8]) { + target.copy_from_slice(self.as_slice()); + } + + fn decode(bytes: &[u8]) -> Self { + Address::from_slice(bytes) + } +} + +impl PrimitiveCodec for bool { + const ENCODED_SIZE: usize = 1; + + fn encode_into(&self, target: &mut [u8]) { + target[0] = *self as u8; + } + + fn decode(bytes: &[u8]) -> Self { + bytes[0] != 0 + } +} + +impl PrimitiveCodec for u64 { + const ENCODED_SIZE: usize = 8; + + fn encode_into(&self, target: &mut [u8]) { + target.copy_from_slice(&self.to_be_bytes()); + } + + fn decode(bytes: &[u8]) -> Self { + u64::from_be_bytes(bytes.try_into().unwrap()) + } +} + +impl PrimitiveCodec for u32 { + const ENCODED_SIZE: usize = 4; + + fn encode_into(&self, target: &mut [u8]) { + target.copy_from_slice(&self.to_be_bytes()); + } + + fn decode(bytes: &[u8]) -> Self { + u32::from_be_bytes(bytes.try_into().unwrap()) + } +} + +impl PrimitiveCodec for u16 { + const ENCODED_SIZE: usize = 2; + + fn encode_into(&self, target: &mut [u8]) { + target.copy_from_slice(&self.to_be_bytes()); + } + + fn decode(bytes: &[u8]) -> Self { + u16::from_be_bytes(bytes.try_into().unwrap()) + } +} + +impl PrimitiveCodec for u8 { + const ENCODED_SIZE: usize = 1; + + fn encode_into(&self, target: &mut [u8]) { + target[0] = *self; + } + + fn decode(bytes: &[u8]) -> Self { + bytes[0] + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::storage::mock::MockStorage; + + #[test] + fn read_write_u256() { + let mut sdk = MockStorage::new(); + let counter = StoragePrimitive::::new(U256::from(1), 0); + let accessor = as StorageLayout>::access(counter); + + let value = accessor.get(&sdk); + assert_eq!(value, U256::ZERO); + + accessor.set(&mut sdk, U256::from(1)); + let value = accessor.get(&sdk); + assert_eq!(value, U256::from(1)); + } + + #[test] + fn set_and_get_work_correctly() { + let mut sdk = MockStorage::new(); + let counter = StoragePrimitive::::new(U256::from(2), 0); + let new_value = U256::from(12345); + let accessor = as StorageLayout>::access(counter); + + // Act: Set the value + accessor.set(&mut sdk, new_value); + + // Assert: Read it back + let read_value = accessor.get(&sdk); + assert_eq!(read_value, new_value); + + // Assert: Check raw slot content + assert_eq!(sdk.get_slot(U256::from(2)), new_value); + } + + #[test] + fn packed_values_do_not_interfere() { + let mut sdk = MockStorage::new(); + let slot = U256::from(3); + + // Layout: [ u64 (offset 23) | bool (offset 31) ] + let flag_accessor = + as StorageLayout>::access(StoragePrimitive::::new( + slot, 31, + )); + let timestamp_accessor = as StorageLayout>::access( + StoragePrimitive::::new(slot, 23), + ); + + // Act + flag_accessor.set(&mut sdk, true); + timestamp_accessor.set(&mut sdk, 0xDEADBEEFCAFEBABE); + + // Assert + assert!(flag_accessor.get(&sdk)); + assert_eq!(timestamp_accessor.get(&sdk), 0xDEADBEEFCAFEBABE); + + // Assert raw slot content via snapshot + let expected_hex = "0000000000000000000000000000000000000000000000deadbeefcafebabe01"; + assert_eq!(sdk.get_slot_hex(U256::from(3)), expected_hex); + } + + #[test] + fn set_optimizes_for_full_width_types() { + let mut sdk = MockStorage::new(); + // Initialize slot with junk data + sdk.init_slot( + U256::from(3), + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ); + + let accessor = as StorageLayout>::access( + StoragePrimitive::::new(U256::from(4), 0), + ); + let new_value = U256::from(1); + + // Act: This `set` should NOT read the old value but overwrite it completely. + accessor.set(&mut sdk, new_value); + + // Assert + assert_eq!( + sdk.get_slot_hex(U256::from(4)), + "0000000000000000000000000000000000000000000000000000000000000001" + ); + } + + #[test] + fn different_primitive_types() { + let mut sdk = MockStorage::new(); + let slot = U256::from(5); + + // Test different primitive types + let bool_accessor = + as StorageLayout>::access(StoragePrimitive::::new( + slot, 31, + )); + let u8_accessor = + as StorageLayout>::access(StoragePrimitive::::new(slot, 30)); + let u16_accessor = as StorageLayout>::access( + StoragePrimitive::::new(slot, 28), + ); + let u32_accessor = as StorageLayout>::access( + StoragePrimitive::::new(slot, 24), + ); + + // Set values + bool_accessor.set(&mut sdk, true); + u8_accessor.set(&mut sdk, 0xFF); + u16_accessor.set(&mut sdk, 0xABCD); + u32_accessor.set(&mut sdk, 0x12345678); + + // Verify values + assert!(bool_accessor.get(&sdk)); + assert_eq!(u8_accessor.get(&sdk), 0xFF); + assert_eq!(u16_accessor.get(&sdk), 0xABCD); + assert_eq!(u32_accessor.get(&sdk), 0x12345678); + } +} diff --git a/crates/sdk/src/storage/vec.rs b/crates/sdk/src/storage/vec.rs new file mode 100644 index 000000000..ba171593d --- /dev/null +++ b/crates/sdk/src/storage/vec.rs @@ -0,0 +1,339 @@ +use crate::{ + keccak256, + storage::{StorageDescriptor, StorageLayout, StorageOps, VecAccess}, + B256, U256, +}; +use core::marker::PhantomData; +use fluentbase_types::StorageAPI; + +// --- 1. Vec Descriptor --- + +/// A descriptor for a dynamic vector in storage. +/// Follows Solidity's dynamic array storage layout: +/// - Length stored at base slot +/// - Elements start at keccak256(base_slot) +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct StorageVec { + base_slot: U256, + _marker: PhantomData, +} + +impl StorageVec { + /// Creates a new vector descriptor at the given base slot. + pub const fn new(base_slot: U256) -> Self { + Self { + base_slot, + _marker: PhantomData, + } + } + + /// Computes the storage slot for vector elements. + /// Elements are stored starting at keccak256(base_slot). + fn elements_base_slot(&self) -> U256 { + // In Solidity, dynamic array elements start at keccak256(p) + // where p is the base slot padded to 32 bytes + let hash = keccak256(self.base_slot.to_be_bytes::<32>()); + U256::from_be_bytes(hash.0) + } + + /// Calculates the slot and offset for an element at the given index. + fn element_location(&self, index: u64) -> (U256, u8) + where + T: StorageLayout, + { + let elements_base = self.elements_base_slot(); + + if T::REQUIRED_SLOTS == 0 { + // Primitive types that can potentially be packed + if T::ENCODED_SIZE < 32 { + // Packable primitive - multiple elements per slot + let elements_per_slot = 32 / T::ENCODED_SIZE; + let slot_index = index / elements_per_slot as u64; + let position_in_slot = index % elements_per_slot as u64; + + // Pack from left to right for arrays (high bytes to low bytes) + // First element at offset 0, second at offset T::ENCODED_SIZE, etc. + let offset = (32 - (position_in_slot + 1) * T::ENCODED_SIZE as u64) as u8; + let slot = elements_base + U256::from(slot_index); + + (slot, offset) + } else { + // Full-width primitive (32 bytes) - one per slot + let slot = elements_base + U256::from(index); + (slot, 0) + } + } else { + // Complex types - use REQUIRED_SLOTS slots per element + let slot = elements_base + U256::from(index * T::REQUIRED_SLOTS as u64); + (slot, 0) + } + } +} + +impl StorageDescriptor for StorageVec { + fn new(slot: U256, offset: u8) -> Self { + debug_assert_eq!(offset, 0, "vectors always start at slot boundary"); + Self { + base_slot: slot, + _marker: PhantomData, + } + } + + fn slot(&self) -> U256 { + self.base_slot + } + + fn offset(&self) -> u8 { + 0 + } +} + +// --- 2. VecAccess Implementation --- + +impl VecAccess for StorageVec +where + T::Descriptor: StorageDescriptor, +{ + fn len(&self, sdk: &S) -> u64 { + // Length is stored at the base slot + let length_word = sdk.sload(self.base_slot); + // Convert from B256 to u64 (taking the least significant 8 bytes) + let bytes = length_word.as_slice(); + let mut len_bytes = [0u8; 8]; + len_bytes.copy_from_slice(&bytes[24..32]); + u64::from_be_bytes(len_bytes) + } + + fn is_empty(&self, sdk: &S) -> bool { + self.len(sdk) == 0 + } + + fn at(&self, index: u64) -> T::Accessor { + let (slot, offset) = self.element_location(index); + let element_descriptor = T::Descriptor::new(slot, offset); + T::access(element_descriptor) + } + + fn push(&self, sdk: &mut S) -> T::Accessor { + // Get current length + let current_len = self.len(sdk); + + // Update length (increment by 1) + let new_len = current_len + 1; + let mut len_bytes = [0u8; 32]; + len_bytes[24..32].copy_from_slice(&new_len.to_be_bytes()); + sdk.sstore(self.base_slot, B256::from(len_bytes)); + + // Return accessor to the new element (at index = old length) + let (slot, offset) = self.element_location(current_len); + let element_descriptor = T::Descriptor::new(slot, offset); + T::access(element_descriptor) + } + + fn pop(&self, sdk: &mut S) { + let current_len = self.len(sdk); + if current_len == 0 { + return; // Nothing to pop + } + + // Update length (decrement by 1) + let new_len = current_len - 1; + let mut len_bytes = [0u8; 32]; + len_bytes[24..32].copy_from_slice(&new_len.to_be_bytes()); + sdk.sstore(self.base_slot, B256::from(len_bytes)); + + // Note: We don't clear the storage slot for gas optimization + // This matches Solidity's behavior + } + + fn clear(&self, sdk: &mut S) { + // Set the length to 0 + sdk.sstore(self.base_slot, B256::ZERO); + } +} + +// --- 3. StorageLayout Implementation --- + +impl StorageLayout for StorageVec +where + T::Descriptor: StorageDescriptor, +{ + type Descriptor = StorageVec; + type Accessor = Self; + + // Dynamic vectors only need 1 slot for the length + // Elements are stored separately at keccak256(base_slot) + const REQUIRED_SLOTS: usize = 1; + + const ENCODED_SIZE: usize = 32; // Only the length is stored inline + + fn access(descriptor: Self::Descriptor) -> Self::Accessor { + descriptor + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + keccak256, + storage::{mock::MockStorage, primitive::StoragePrimitive, PrimitiveAccess}, + }; + + #[test] + fn test_vec_push_and_access() { + let mut sdk = MockStorage::new(); + let vec = StorageVec::>::new(U256::from(100)); + + // Initially empty + assert_eq!(vec.len(&sdk), 0); + assert!(vec.is_empty(&sdk)); + + // Push the first element + vec.push(&mut sdk).set(&mut sdk, U256::from(111)); + assert_eq!(vec.len(&sdk), 1); + + // Push the second element + vec.push(&mut sdk).set(&mut sdk, U256::from(222)); + assert_eq!(vec.len(&sdk), 2); + + // Access elements - need to check bounds first + let current_len = vec.len(&sdk); + assert!( + 0 < current_len, + "vector index out of bounds: index 0 >= length {current_len}" + ); + assert!( + 1 < current_len, + "vector index out of bounds: index 1 >= length {current_len}" + ); + + assert_eq!(vec.at(0).get(&sdk), U256::from(111)); + assert_eq!(vec.at(1).get(&sdk), U256::from(222)); + } + + #[test] + fn test_vec_pop() { + let mut sdk = MockStorage::new(); + let vec = StorageVec::>::new(U256::from(200)); + + // Push elements + vec.push(&mut sdk).set(&mut sdk, U256::from(100)); + vec.push(&mut sdk).set(&mut sdk, U256::from(200)); + vec.push(&mut sdk).set(&mut sdk, U256::from(300)); + assert_eq!(vec.len(&sdk), 3); + + // Pop one element + vec.pop(&mut sdk); + assert_eq!(vec.len(&sdk), 2); + + // Remaining elements still accessible + assert_eq!(vec.at(0).get(&sdk), U256::from(100)); + assert_eq!(vec.at(1).get(&sdk), U256::from(200)); + } + + #[test] + fn test_vec_packed_elements() { + let mut sdk = MockStorage::new(); + let vec = StorageVec::>::new(U256::from(300)); + + // Push multiple u64 values (should pack) + vec.push(&mut sdk).set(&mut sdk, 0x1111111111111111u64); + vec.push(&mut sdk).set(&mut sdk, 0x2222222222222222u64); + vec.push(&mut sdk).set(&mut sdk, 0x3333333333333333u64); + vec.push(&mut sdk).set(&mut sdk, 0x4444444444444444u64); + + assert_eq!(vec.len(&sdk), 4); + + // Verify packing by checking the storage + // Elements should be packed in the first slot after keccak(base_slot) + let elements_base = { + let hash = keccak256(U256::from(300).to_be_bytes::<32>()); + U256::from_be_bytes(hash.0) + }; + + // All 4 u64 values should be packed in one slot + let slot_content = sdk.get_slot_hex(elements_base); + assert_eq!( + slot_content, + "4444444444444444333333333333333322222222222222221111111111111111" + ); + } + + #[test] + fn test_vec_packed_elements2() { + let mut sdk = MockStorage::new(); + let vec = StorageVec::>::new(U256::from(300)); + + vec.push(&mut sdk).set(&mut sdk, 0x1111111111111111u64); + vec.push(&mut sdk).set(&mut sdk, 0x2222222222222222u64); + vec.push(&mut sdk).set(&mut sdk, 0x3333333333333333u64); + vec.push(&mut sdk).set(&mut sdk, 0x4444444444444444u64); + + let elements_base = { + let hash = keccak256(U256::from(300).to_be_bytes::<32>()); + U256::from_be_bytes(hash.0) + }; + + // Добавьте отладочный вывод + println!("Elements base slot: {elements_base:?}"); + let raw_value = sdk.get_slot(elements_base); + println!("Raw U256 value: {raw_value:?}"); + println!("As hex: {}", sdk.get_slot_hex(elements_base)); + + // Проверим offset для каждого элемента + for i in 0..4 { + let (slot, offset) = vec.element_location(i); + println!("Element {i}: slot={slot:?}, offset={offset}"); + } + } + + #[test] + fn test_vec_clear() { + let mut sdk = MockStorage::new(); + let vec = StorageVec::>::new(U256::from(400)); + + // Push elements + vec.push(&mut sdk).set(&mut sdk, U256::from(1)); + vec.push(&mut sdk).set(&mut sdk, U256::from(2)); + assert_eq!(vec.len(&sdk), 2); + + // Clear + vec.clear(&mut sdk); + assert_eq!(vec.len(&sdk), 0); + assert!(vec.is_empty(&sdk)); + } + + #[test] + fn test_vec_bounds_check() { + let mut sdk = MockStorage::new(); + let vec = StorageVec::>::new(U256::from(500)); + + vec.push(&mut sdk).set(&mut sdk, U256::from(100)); + + // Test that we can access a valid index + assert_eq!(vec.at(0).get(&sdk), U256::from(100)); + + // Note: at() method itself doesn't perform bound's checking since it doesn't have access to SDK + // Bounds checking would need to be done by the caller or in the accessor methods + } + + #[test] + fn test_nested_vec() { + let mut sdk = MockStorage::new(); + let vec = StorageVec::>>::new(U256::from(600)); + + // Push a nested vector + let inner_vec = vec.push(&mut sdk); + + // Push elements to the inner vector + inner_vec.push(&mut sdk).set(&mut sdk, U256::from(10)); + inner_vec.push(&mut sdk).set(&mut sdk, U256::from(20)); + + // Verify + assert_eq!(vec.len(&sdk), 1); + assert_eq!(vec.at(0).len(&sdk), 2); + assert_eq!(vec.at(0).at(0).get(&sdk), U256::from(10)); + assert_eq!(vec.at(0).at(1).get(&sdk), U256::from(20)); + } +} From cbd801f00abf05a76dc194d6357034bf0a21523b Mon Sep 17 00:00:00 2001 From: d1r1 Date: Thu, 11 Sep 2025 15:17:15 +0400 Subject: [PATCH 04/16] refactor(sdk-derive): depricate solidity_storage macro --- crates/sdk-derive/derive-core/src/lib.rs | 6 +- ..._storage__tests__direct_storage_types.snap | 55 ------------------- ...storage_legacy__tests__array_storage.snap} | 2 +- ...e_legacy__tests__fixed_bytes_storage.snap} | 2 +- ...orage_legacy__tests__mapping_storage.snap} | 2 +- ...age_legacy__tests__primitive_storage.snap} | 2 +- .../src/{storage.rs => storage_legacy.rs} | 0 crates/sdk-derive/src/lib.rs | 4 +- 8 files changed, 11 insertions(+), 62 deletions(-) delete mode 100644 crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__direct_storage_types.snap rename crates/sdk-derive/derive-core/src/snapshots/{fluentbase_sdk_derive_core__storage__tests__array_storage.snap => fluentbase_sdk_derive_core__storage_legacy__tests__array_storage.snap} (97%) rename crates/sdk-derive/derive-core/src/snapshots/{fluentbase_sdk_derive_core__storage__tests__fixed_bytes_storage.snap => fluentbase_sdk_derive_core__storage_legacy__tests__fixed_bytes_storage.snap} (98%) rename crates/sdk-derive/derive-core/src/snapshots/{fluentbase_sdk_derive_core__storage__tests__mapping_storage.snap => fluentbase_sdk_derive_core__storage_legacy__tests__mapping_storage.snap} (99%) rename crates/sdk-derive/derive-core/src/snapshots/{fluentbase_sdk_derive_core__storage__tests__primitive_storage.snap => fluentbase_sdk_derive_core__storage_legacy__tests__primitive_storage.snap} (97%) rename crates/sdk-derive/derive-core/src/{storage.rs => storage_legacy.rs} (100%) diff --git a/crates/sdk-derive/derive-core/src/lib.rs b/crates/sdk-derive/derive-core/src/lib.rs index 2f977a8d4..370f04f6d 100644 --- a/crates/sdk-derive/derive-core/src/lib.rs +++ b/crates/sdk-derive/derive-core/src/lib.rs @@ -9,5 +9,9 @@ mod method; pub mod router; mod signature; pub mod sol_input; -pub mod storage; +#[deprecated( + note = "Use `fluentbase_sdk_derive_core::storage` instead", + since = "0.4.5-dev" +)] +pub mod storage_legacy; mod utils; diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__direct_storage_types.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__direct_storage_types.snap deleted file mode 100644 index a507524f6..000000000 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__direct_storage_types.snap +++ /dev/null @@ -1,55 +0,0 @@ ---- -source: crates/sdk-derive/derive-core/src/storage.rs -expression: formatted ---- -pub struct Owner {} -impl Owner { - const SLOT: fluentbase_sdk::U256 = fluentbase_sdk::U256::from_limbs([ - 0u64, 0u64, 0u64, 0u64, - ]); - fn get(sdk: &SDK) -> Address { - let key = Self::key(sdk); -
>::get(sdk, key) - } - fn set(sdk: &mut SDK, value: Address) { - let key = Self::key(sdk); -
>::set(sdk, key, value) - } - fn key(_sdk: &SDK) -> fluentbase_sdk::U256 { - Self::SLOT - } -} -pub struct Paused {} -impl Paused { - const SLOT: fluentbase_sdk::U256 = fluentbase_sdk::U256::from_limbs([ - 1u64, 0u64, 0u64, 0u64, - ]); - fn get(sdk: &SDK) -> bool { - let key = Self::key(sdk); - >::get(sdk, key) - } - fn set(sdk: &mut SDK, value: bool) { - let key = Self::key(sdk); - >::set(sdk, key, value) - } - fn key(_sdk: &SDK) -> fluentbase_sdk::U256 { - Self::SLOT - } -} -pub struct SmallValue {} -impl SmallValue { - const SLOT: fluentbase_sdk::U256 = fluentbase_sdk::U256::from_limbs([ - 2u64, 0u64, 0u64, 0u64, - ]); - fn get(sdk: &SDK) -> u8 { - let key = Self::key(sdk); - >::get(sdk, key) - } - fn set(sdk: &mut SDK, value: u8) { - let key = Self::key(sdk); - >::set(sdk, key, value) - } - fn key(_sdk: &SDK) -> fluentbase_sdk::U256 { - Self::SLOT - } -} diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__array_storage.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__array_storage.snap similarity index 97% rename from crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__array_storage.snap rename to crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__array_storage.snap index 3890bdc7a..b81254eef 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__array_storage.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__array_storage.snap @@ -1,5 +1,5 @@ --- -source: crates/sdk-derive/derive-core/src/storage.rs +source: crates/sdk-derive/derive-core/src/storage_legacy.rs expression: formatted --- pub struct Arr {} diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__fixed_bytes_storage.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__fixed_bytes_storage.snap similarity index 98% rename from crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__fixed_bytes_storage.snap rename to crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__fixed_bytes_storage.snap index 8163c0a9f..6648cba40 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__fixed_bytes_storage.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__fixed_bytes_storage.snap @@ -1,5 +1,5 @@ --- -source: crates/sdk-derive/derive-core/src/storage.rs +source: crates/sdk-derive/derive-core/src/storage_legacy.rs expression: formatted --- pub struct CustomBytes1 {} diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__mapping_storage.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__mapping_storage.snap similarity index 99% rename from crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__mapping_storage.snap rename to crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__mapping_storage.snap index 20242436d..feab2ad44 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__mapping_storage.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__mapping_storage.snap @@ -1,5 +1,5 @@ --- -source: crates/sdk-derive/derive-core/src/storage.rs +source: crates/sdk-derive/derive-core/src/storage_legacy.rs expression: formatted --- pub struct Balance {} diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__primitive_storage.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__primitive_storage.snap similarity index 97% rename from crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__primitive_storage.snap rename to crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__primitive_storage.snap index 2bdb757d9..6e3ff21a6 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__primitive_storage.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage_legacy__tests__primitive_storage.snap @@ -1,5 +1,5 @@ --- -source: crates/sdk-derive/derive-core/src/storage.rs +source: crates/sdk-derive/derive-core/src/storage_legacy.rs expression: formatted --- pub struct Owner {} diff --git a/crates/sdk-derive/derive-core/src/storage.rs b/crates/sdk-derive/derive-core/src/storage_legacy.rs similarity index 100% rename from crates/sdk-derive/derive-core/src/storage.rs rename to crates/sdk-derive/derive-core/src/storage_legacy.rs diff --git a/crates/sdk-derive/src/lib.rs b/crates/sdk-derive/src/lib.rs index 73edb4bca..390d97dd0 100644 --- a/crates/sdk-derive/src/lib.rs +++ b/crates/sdk-derive/src/lib.rs @@ -1,5 +1,5 @@ use crate::contract::impl_derive_contract; -use fluentbase_sdk_derive_core::{client, router, storage}; +use fluentbase_sdk_derive_core::{client, router, storage_legacy}; use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; use quote::{quote, ToTokens}; @@ -235,7 +235,7 @@ pub fn client(attr: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro] #[proc_macro_error] pub fn solidity_storage(input: TokenStream) -> TokenStream { - let storage = parse_macro_input!(input as storage::Storage); + let storage = parse_macro_input!(input as storage_legacy::Storage); storage.to_token_stream().into() } From ad8276ca432ed4df8878aa6c1b688d1a9c653754 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Fri, 12 Sep 2025 00:37:47 +0400 Subject: [PATCH 05/16] feat(sdk-derive): add solidity-compatible storage --- crates/sdk-derive/derive-core/src/lib.rs | 1 + ...ore__storage__tests__composite_config.snap | 233 +++++++++++ ...ore__storage__tests__contract_storage.snap | 159 ++++++++ ...ore__storage__tests__nested_composite.snap | 157 ++++++++ ...ore__storage__tests__packed_composite.snap | 371 +++++++++++++++++ crates/sdk-derive/derive-core/src/storage.rs | 379 ++++++++++++++++++ crates/sdk-derive/src/lib.rs | 11 + 7 files changed, 1311 insertions(+) create mode 100644 crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__composite_config.snap create mode 100644 crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__contract_storage.snap create mode 100644 crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__nested_composite.snap create mode 100644 crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__packed_composite.snap create mode 100644 crates/sdk-derive/derive-core/src/storage.rs diff --git a/crates/sdk-derive/derive-core/src/lib.rs b/crates/sdk-derive/derive-core/src/lib.rs index 370f04f6d..875f62465 100644 --- a/crates/sdk-derive/derive-core/src/lib.rs +++ b/crates/sdk-derive/derive-core/src/lib.rs @@ -9,6 +9,7 @@ mod method; pub mod router; mod signature; pub mod sol_input; +pub mod storage; #[deprecated( note = "Use `fluentbase_sdk_derive_core::storage` instead", since = "0.4.5-dev" diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__composite_config.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__composite_config.snap new file mode 100644 index 000000000..e37be7365 --- /dev/null +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__composite_config.snap @@ -0,0 +1,233 @@ +--- +source: crates/sdk-derive/derive-core/src/storage.rs +expression: formatted +--- +impl Config { + pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); + pub fn new(slot: fluentbase_sdk::U256, offset: u8) -> Self { + let mut current_slot = slot; + let mut current_offset: u8 = offset; + let owner_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + let version_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + let max_supply_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + Self { + owner: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + owner_layout.0, + owner_layout.1, + ), + version: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + version_layout.0, + version_layout.1, + ), + max_supply: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + max_supply_layout.0, + max_supply_layout.1, + ), + } + } + const fn calculate_required_slots() -> usize { + let mut current_slot: usize = 0; + let mut current_offset: usize = 0; + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + if current_offset > 0 { current_slot + 1 } else { current_slot } + } + ///Returns an accessor for the owner field + #[inline] + pub fn owner( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.owner) + } + ///Returns an accessor for the version field + #[inline] + pub fn version( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.version) + } + ///Returns an accessor for the max_supply field + #[inline] + pub fn max_supply( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.max_supply) + } +} +impl fluentbase_sdk::storage::composite::CompositeStorage for Config { + const REQUIRED_SLOTS: usize = Self::REQUIRED_SLOTS; + fn from_slot(base_slot: fluentbase_sdk::U256) -> Self { + Self::new(base_slot, 0) + } +} diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__contract_storage.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__contract_storage.snap new file mode 100644 index 000000000..551db8648 --- /dev/null +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__contract_storage.snap @@ -0,0 +1,159 @@ +--- +source: crates/sdk-derive/derive-core/src/storage.rs +expression: formatted +--- +impl Storage { + pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); + pub fn new(sdk: SDK, slot: fluentbase_sdk::U256, offset: u8) -> Self { + let mut current_slot = slot; + let mut current_offset: u8 = offset; + let owner_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + let counter_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + Self { + sdk, + owner: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + owner_layout.0, + owner_layout.1, + ), + counter: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + counter_layout.0, + counter_layout.1, + ), + } + } + const fn calculate_required_slots() -> usize { + let mut current_slot: usize = 0; + let mut current_offset: usize = 0; + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + if current_offset > 0 { current_slot + 1 } else { current_slot } + } + ///Returns an accessor for the owner field + #[inline] + pub fn owner( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.owner) + } + ///Returns an accessor for the counter field + #[inline] + pub fn counter( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.counter) + } +} diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__nested_composite.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__nested_composite.snap new file mode 100644 index 000000000..a4de4becf --- /dev/null +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__nested_composite.snap @@ -0,0 +1,157 @@ +--- +source: crates/sdk-derive/derive-core/src/storage.rs +expression: formatted +--- +impl NestedStructTest { + pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); + pub fn new(sdk: SDK, slot: fluentbase_sdk::U256, offset: u8) -> Self { + let mut current_slot = slot; + let mut current_offset: u8 = offset; + let counter_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + let config_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + Self { + sdk, + counter: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + counter_layout.0, + counter_layout.1, + ), + config: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + config_layout.0, + config_layout.1, + ), + } + } + const fn calculate_required_slots() -> usize { + let mut current_slot: usize = 0; + let mut current_offset: usize = 0; + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + if current_offset > 0 { current_slot + 1 } else { current_slot } + } + ///Returns an accessor for the counter field + #[inline] + pub fn counter( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.counter) + } + ///Returns an accessor for the config field + #[inline] + pub fn config( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.config) + } +} diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__packed_composite.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__packed_composite.snap new file mode 100644 index 000000000..c47a1b865 --- /dev/null +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__packed_composite.snap @@ -0,0 +1,371 @@ +--- +source: crates/sdk-derive/derive-core/src/storage.rs +expression: formatted +--- +impl PackedConfig { + pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); + pub fn new(slot: fluentbase_sdk::U256, offset: u8) -> Self { + let mut current_slot = slot; + let mut current_offset: u8 = offset; + let is_active_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + let is_paused_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + let version_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + let flags_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + let owner_layout = { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let layout = if required_slots == 0 { + if current_offset + encoded_size <= 32 { + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + layout + }; + Self { + is_active: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + is_active_layout.0, + is_active_layout.1, + ), + is_paused: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + is_paused_layout.0, + is_paused_layout.1, + ), + version: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + version_layout.0, + version_layout.1, + ), + flags: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + flags_layout.0, + flags_layout.1, + ), + owner: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + owner_layout.0, + owner_layout.1, + ), + } + } + const fn calculate_required_slots() -> usize { + let mut current_slot: usize = 0; + let mut current_offset: usize = 0; + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + { + let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + if required_slots == 0 { + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + if current_offset > 0 { current_slot + 1 } else { current_slot } + } + ///Returns an accessor for the is_active field + #[inline] + pub fn is_active( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.is_active) + } + ///Returns an accessor for the is_paused field + #[inline] + pub fn is_paused( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.is_paused) + } + ///Returns an accessor for the version field + #[inline] + pub fn version( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.version) + } + ///Returns an accessor for the flags field + #[inline] + pub fn flags( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.flags) + } + ///Returns an accessor for the owner field + #[inline] + pub fn owner( + &self, + ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { + as fluentbase_sdk::storage::StorageLayout>::access(self.owner) + } +} +impl fluentbase_sdk::storage::composite::CompositeStorage for PackedConfig { + const REQUIRED_SLOTS: usize = Self::REQUIRED_SLOTS; + fn from_slot(base_slot: fluentbase_sdk::U256) -> Self { + Self::new(base_slot, 0) + } +} diff --git a/crates/sdk-derive/derive-core/src/storage.rs b/crates/sdk-derive/derive-core/src/storage.rs new file mode 100644 index 000000000..7e49640df --- /dev/null +++ b/crates/sdk-derive/derive-core/src/storage.rs @@ -0,0 +1,379 @@ +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::{Data, DeriveInput, Fields, GenericParam}; + +pub fn process_storage_layout(input: DeriveInput) -> Result { + let has_sdk = + input.generics.params.iter().any( + |param| matches!(param, GenericParam::Type(type_param) if type_param.ident == "SDK"), + ); + + if has_sdk { + process_contract_storage(input) + } else { + process_composite_storage(input) + } +} + +fn process_contract_storage(input: DeriveInput) -> Result { + let name = &input.ident; + let fields = extract_storage_fields(&input)?; + + let constructor = generate_constructor_body(&fields, true); + let accessors = generate_accessor_methods(&fields, true); + let slots_calculation = generate_const_slots_calculation(&fields); + + Ok(quote! { + impl #name { + pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); + + pub fn new(sdk: SDK, slot: fluentbase_sdk::U256, offset: u8) -> Self { + #constructor + } + + #slots_calculation + #accessors + } + }) +} + +fn process_composite_storage(input: DeriveInput) -> Result { + let name = &input.ident; + let fields = extract_storage_fields(&input)?; + + let slots_calculation = generate_const_slots_calculation(&fields); + let constructor = generate_constructor_body(&fields, false); + let accessors = generate_accessor_methods(&fields, false); + + Ok(quote! { + impl #name { + pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); + + pub fn new(slot: fluentbase_sdk::U256, offset: u8) -> Self { + #constructor + } + + #slots_calculation + #accessors + } + + impl fluentbase_sdk::storage::composite::CompositeStorage for #name { + const REQUIRED_SLOTS: usize = Self::REQUIRED_SLOTS; + + fn from_slot(base_slot: fluentbase_sdk::U256) -> Self { + Self::new(base_slot, 0) + } + } + }) +} + +fn generate_constructor_body( + fields: &syn::punctuated::Punctuated, + has_sdk: bool, +) -> TokenStream2 { + let mut layout_calculations = Vec::new(); + let mut field_assignments = Vec::new(); + + for field in fields { + let field_name = field.ident.as_ref().expect("Named fields required"); + + if has_sdk && field_name == "sdk" { + continue; + } + + let field_type = &field.ty; + let layout_var = quote::format_ident!("{}_layout", field_name); + + layout_calculations.push(generate_layout_calculation(&layout_var, field_type)); + field_assignments.push(generate_field_init(field_name, field_type, &layout_var)); + } + + let sdk_assignment = if has_sdk { + quote! { sdk, } + } else { + quote! {} + }; + + quote! { + let mut current_slot = slot; + let mut current_offset: u8 = offset; + + #(#layout_calculations)* + + Self { + #sdk_assignment + #(#field_assignments),* + } + } +} + +fn generate_layout_calculation(layout_var: &syn::Ident, field_type: &syn::Type) -> TokenStream2 { + quote! { + let #layout_var = { + let encoded_size = <#field_type as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; + let required_slots = <#field_type as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + + let layout = if required_slots == 0 { + // StoragePrimitive type - try to pack + if current_offset + encoded_size <= 32 { + // Fits in current slot + // Calculate offset from the RIGHT edge (Solidity packs right to left) + let actual_offset = 32 - current_offset - encoded_size; + let result = (current_slot, actual_offset); + current_offset += encoded_size; + result + } else { + // Doesn't fit, move to next slot + current_slot = current_slot + fluentbase_sdk::U256::from(1); + // First element in new slot goes to rightmost position + let actual_offset = 32 - encoded_size; + current_offset = encoded_size; + (current_slot, actual_offset) + } + } else { + // Composite type - needs its own slot(s) + if current_offset > 0 { + // If we were packing, move to next slot + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_offset = 0; + result + }; + + layout + }; + } +} + +fn generate_field_init( + field_name: &syn::Ident, + field_type: &syn::Type, + layout_var: &syn::Ident, +) -> TokenStream2 { + quote! { + #field_name: <<#field_type as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + #layout_var.0, + #layout_var.1 + ) + } +} + +fn generate_const_slots_calculation( + fields: &syn::punctuated::Punctuated, +) -> TokenStream2 { + let mut field_calculations = Vec::new(); + + for field in fields { + let field_type = &field.ty; + + field_calculations.push(quote! { + { + let encoded_size = <#field_type as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; + let required_slots = <#field_type as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + + if required_slots == 0 { + // StoragePrimitive type + if current_offset + encoded_size <= 32 { + current_offset += encoded_size; + } else { + current_slot += 1; + current_offset = encoded_size; + } + } else { + // Composite type + if current_offset > 0 { + current_slot += 1; + current_offset = 0; + } + current_slot += required_slots; + current_offset = 0; + } + } + }); + } + + quote! { + const fn calculate_required_slots() -> usize { + let mut current_slot: usize = 0; + let mut current_offset: usize = 0; + + #(#field_calculations)* + + if current_offset > 0 { + current_slot + 1 + } else { + current_slot + } + } + } +} + +// Keep the existing helper functions unchanged +fn extract_named_fields( + input: &DeriveInput, +) -> Result<&syn::punctuated::Punctuated, syn::Error> { + match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => Ok(&fields.named), + _ => Err(syn::Error::new_spanned( + input, + "StorageLayout only supports structs with named fields", + )), + }, + _ => Err(syn::Error::new_spanned( + input, + "StorageLayout can only be derived for structs", + )), + } +} + +fn extract_storage_fields( + input: &DeriveInput, +) -> Result, syn::Error> { + let fields = extract_named_fields(input)?; + + let mut storage_fields = syn::punctuated::Punctuated::new(); + for field in fields { + if let Some(field_name) = &field.ident { + if field_name != "sdk" { + storage_fields.push(field.clone()); + } + } + } + Ok(storage_fields) +} + +fn generate_accessor_methods( + fields: &syn::punctuated::Punctuated, + has_sdk: bool, +) -> TokenStream2 { + let mut methods = Vec::new(); + + for field in fields { + let field_name = field.ident.as_ref().expect("Named fields required"); + + if has_sdk && field_name == "sdk" { + continue; + } + + let field_type = &field.ty; + let doc_string = format!("Returns an accessor for the {} field", field_name); + + methods.push(quote! { + #[doc = #doc_string] + #[inline] + pub fn #field_name(&self) -> <#field_type as fluentbase_sdk::storage::StorageLayout>::Accessor { + <#field_type as fluentbase_sdk::storage::StorageLayout>::access(self.#field_name) + } + }); + } + + quote! { + #(#methods)* + } +} + +#[cfg(test)] +mod tests { + use super::*; + use insta::assert_snapshot; + use syn::{parse_file, parse_quote}; + + #[test] + fn test_composite_type() { + let input: DeriveInput = parse_quote! { + struct Config { + owner: StoragePrimitive
, + version: StoragePrimitive, + max_supply: StoragePrimitive, + } + }; + + let result = process_storage_layout(input).unwrap(); + let file = parse_file(&result.to_string()).unwrap(); + let formatted = prettyplease::unparse(&file); + + assert_snapshot!("composite_config", formatted); + } + + #[test] + fn test_contract_with_sdk() { + let input: DeriveInput = parse_quote! { + struct Storage { + sdk: SDK, + owner: StoragePrimitive
, + counter: StoragePrimitive, + } + }; + + let result = process_storage_layout(input).unwrap(); + let file = parse_file(&result.to_string()).unwrap(); + let formatted = prettyplease::unparse(&file); + + assert_snapshot!("contract_storage", formatted); + } + + #[test] + fn test_nested_composite() { + let input: DeriveInput = parse_quote! { + struct NestedStructTest { + sdk: SDK, + counter: StoragePrimitive, + config: Composite, + } + }; + + let result = process_storage_layout(input).unwrap(); + let file = parse_file(&result.to_string()).unwrap(); + let formatted = prettyplease::unparse(&file); + + assert_snapshot!("nested_composite", formatted); + } + + #[test] + fn test_packed_composite() { + let input: DeriveInput = parse_quote! { + struct PackedConfig { + is_active: StoragePrimitive, + is_paused: StoragePrimitive, + version: StoragePrimitive, + flags: StoragePrimitive, + owner: StoragePrimitive
, + } + }; + + let result = process_storage_layout(input).unwrap(); + let file = parse_file(&result.to_string()).unwrap(); + let formatted = prettyplease::unparse(&file); + + assert_snapshot!("packed_composite", formatted); + } + + #[test] + fn test_unsupported_struct_type() { + let input: DeriveInput = parse_quote! { + struct Storage(SDK); + }; + + let result = process_storage_layout(input); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("named fields")); + } + + #[test] + fn test_enum_type() { + let input: DeriveInput = parse_quote! { + enum Storage { + Variant1, + Variant2, + } + }; + + let result = process_storage_layout(input); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("structs")); + } +} diff --git a/crates/sdk-derive/src/lib.rs b/crates/sdk-derive/src/lib.rs index 390d97dd0..1b67b9f97 100644 --- a/crates/sdk-derive/src/lib.rs +++ b/crates/sdk-derive/src/lib.rs @@ -6,6 +6,7 @@ use quote::{quote, ToTokens}; mod contract; mod utils; +use fluentbase_sdk_derive_core::storage::process_storage_layout; use syn::parse_macro_input; /// Function ID attribute for overriding function selectors in smart contracts. @@ -414,3 +415,13 @@ pub fn contract_macro_derive(input: TokenStream) -> TokenStream { let ast = syn::parse(input).unwrap(); impl_derive_contract(&ast) } + +#[proc_macro_derive(Storage)] +pub fn derive_storage_layout(input: TokenStream) -> TokenStream { + let input = syn::parse(input).unwrap(); + + match process_storage_layout(input) { + Ok(tokens) => tokens.into(), + Err(err) => err.to_compile_error().into(), + } +} From da6deda38f2852f41fbba027abfdbf7e0a0d07d4 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Fri, 12 Sep 2025 00:39:45 +0400 Subject: [PATCH 06/16] refactor(testing): mv storage utils into examples --- Cargo.lock | 2 -- contracts/Cargo.lock | 2 -- crates/testing/Cargo.toml | 2 -- crates/testing/src/lib.rs | 2 -- crates/testing/src/utils.rs | 65 ------------------------------------- 5 files changed, 73 deletions(-) delete mode 100644 crates/testing/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 0d4507ab5..c4821d69f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2341,8 +2341,6 @@ dependencies = [ "hex", "revm", "rwasm", - "serde", - "serde_json", ] [[package]] diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 1fe68e6fa..c4fec6878 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -2055,8 +2055,6 @@ dependencies = [ "hex", "revm", "rwasm", - "serde", - "serde_json", ] [[package]] diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml index cb03dee43..e4ab6c1df 100644 --- a/crates/testing/Cargo.toml +++ b/crates/testing/Cargo.toml @@ -19,8 +19,6 @@ revm = { workspace = true } fluentbase-revm = { workspace = true } rwasm = { workspace = true } hex = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } [features] default = ["std"] diff --git a/crates/testing/src/lib.rs b/crates/testing/src/lib.rs index 4b80f8234..88e215bdc 100644 --- a/crates/testing/src/lib.rs +++ b/crates/testing/src/lib.rs @@ -1,8 +1,6 @@ mod evm; mod host; -mod utils; pub use evm::*; pub use fluentbase_sdk::include_this_wasm; pub use host::*; -pub use utils::*; diff --git a/crates/testing/src/utils.rs b/crates/testing/src/utils.rs deleted file mode 100644 index ab33cc8cf..000000000 --- a/crates/testing/src/utils.rs +++ /dev/null @@ -1,65 +0,0 @@ -use core::str::FromStr; -use fluentbase_sdk::{Address, U256}; -use hashbrown::HashMap; -use serde_json::Value; - -/// Creates storage HashMap from JSON fixture -/// -/// Expected JSON format: {"contract": {"slot":"value"}} -/// ```json -/// { -/// "0x123": { -/// "0x00": "0xabc", -/// "0x01": "0xdef" -/// } -/// } -/// ``` -pub fn storage_from_fixture(json: &str) -> HashMap<(Address, U256), U256> { - let fixture: Value = serde_json::from_str(json).expect("Invalid JSON"); - let mut storage = HashMap::new(); - - if let Some(contracts) = fixture.as_object() { - for (address_str, slots) in contracts { - let address = Address::from_str(address_str).expect("Invalid address"); - - if let Some(slots_map) = slots.as_object() { - for (slot_str, value_str) in slots_map { - let slot = U256::from_str(slot_str).expect("Invalid slot"); - let value = U256::from_str(value_str.as_str().expect("Value must be string")) - .expect("Invalid value"); - - storage.insert((address, slot), value); - } - } - } - } - - storage -} - -/// Pretty-prints storage entries grouped by contract address and sorted by slot. -pub fn format_storage(storage: &HashMap<(Address, U256), U256>) -> String { - if storage.is_empty() { - return " (empty)".to_string(); - } - - let mut entries: Vec<_> = storage.iter().collect(); - entries.sort_by_key(|((addr, slot), _)| (*addr, *slot)); - - let mut result = String::new(); - let mut current_addr: Option
= None; - - for ((addr, slot), value) in entries { - if current_addr != Some(*addr) { - if current_addr.is_some() { - result.push('\n'); - } - result.push_str(&format!(" Contract 0x{:040x}:\n", addr)); - current_addr = Some(*addr); - } - - result.push_str(&format!(" Slot 0x{:064x}: 0x{:064x}\n", slot, value)); - } - - result -} From 05ff219aa2b862a50b9a5b08099298b383fe3912 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Fri, 12 Sep 2025 00:40:35 +0400 Subject: [PATCH 07/16] feat(examples): add storage usage example --- examples/Cargo.lock | 48 ++- examples/Cargo.toml | 1 + examples/storage-usage/Cargo.toml | 29 ++ examples/storage-usage/README.md | 3 + examples/storage-usage/src/lib.rs | 3 + examples/storage-usage/src/nested.rs | 273 +++++++++++++ examples/storage-usage/src/simple.rs | 244 ++++++++++++ examples/storage-usage/src/utils.rs | 99 +++++ examples/storage/Cargo.toml | 2 + examples/storage/lib.rs | 574 +++++++++++++++------------ 10 files changed, 1003 insertions(+), 273 deletions(-) create mode 100644 examples/storage-usage/Cargo.toml create mode 100644 examples/storage-usage/README.md create mode 100644 examples/storage-usage/src/lib.rs create mode 100644 examples/storage-usage/src/nested.rs create mode 100644 examples/storage-usage/src/simple.rs create mode 100644 examples/storage-usage/src/utils.rs diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 452727948..538827dbf 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "alloy-json-abi" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15516116086325c157c18261d768a20677f0f699348000ed391d4ad0dcb82530" +checksum = "125a1c373261b252e53e04d6e92c37d881833afc1315fceab53fd46045695640" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -100,9 +100,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6177ed26655d4e84e00b65cb494d4e0b8830e7cae7ef5d63087d445a2600fb55" +checksum = "bc9485c56de23438127a731a6b4c87803d49faf1a7068dcd1d8768aca3a9edb9" dependencies = [ "alloy-rlp", "bytes", @@ -160,9 +160,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a14f21d053aea4c6630687c2f4ad614bed4c81e14737a9b904798b24f30ea849" +checksum = "d20d867dcf42019d4779519a1ceb55eba8d7f3d0e4f0a89bcba82b8f9eb01e48" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -174,9 +174,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-expander" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34d99282e7c9ef14eb62727981a985a01869e586d1dec729d3bb33679094c100" +checksum = "b74e91b0b553c115d14bd0ed41898309356dc85d0e3d4b9014c4e7715e48c8ad" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", @@ -193,9 +193,9 @@ dependencies = [ [[package]] name = "alloy-sol-macro-input" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda029f955b78e493360ee1d7bd11e1ab9f2a220a5715449babc79d6d0a01105" +checksum = "84194d31220803f5f62d0a00f583fd3a062b36382e2bea446f1af96727754565" dependencies = [ "alloy-json-abi", "const-hex", @@ -211,9 +211,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52db32fbd35a9c0c0e538b58b81ebbae08a51be029e7ad60e08b60481c2ec6c3" +checksum = "fe8c27b3cf6b2bb8361904732f955bc7c05e00be5f469cec7e2280b6167f3ff0" dependencies = [ "serde", "winnow", @@ -221,9 +221,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58377025a47d8b8426b3e4846a251f2c1991033b27f517aade368146f6ab1dfe" +checksum = "f5383d34ea00079e6dd89c652bcbdb764db160cef84e6250926961a0b2295d04" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -1738,11 +1738,23 @@ dependencies = [ name = "fluentbase-examples-storage" version = "0.1.0" dependencies = [ + "alloy-sol-types", "fluentbase-sdk", "fluentbase-sdk-testing", + "hex-literal 1.0.0", "serial_test", ] +[[package]] +name = "fluentbase-examples-storage-sol" +version = "0.1.0" +dependencies = [ + "fluentbase-sdk", + "fluentbase-sdk-testing", + "serde", + "serde_json", +] + [[package]] name = "fluentbase-examples-tiny-keccak" version = "0.1.0" @@ -3946,9 +3958,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "indexmap", "itoa", @@ -4311,9 +4323,9 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ac494e7266fcdd2ad80bf4375d55d27a117ea5c866c26d0e97fe5b3caeeb75" +checksum = "a0b198d366dbec045acfcd97295eb653a7a2b40e4dc764ef1e79aafcad439d3c" dependencies = [ "paste", "proc-macro2", diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 4ed15eb2d..3f805e603 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -25,6 +25,7 @@ solana-bincode = { git = "https://github.com/fluentlabs-xyz/agave", branch = "fe hex-literal = { version = "0.4.1", default-features = false } alloy-sol-types = { version = "1.2.0", default-features = false, features = ["json"] } serde = { version = "1.0.203", default-features = false, features = ["derive", "rc"] } +serde_json = { version = "1.0", default-features = false, features = ["alloc"] } tiny-keccak = { version = "2.0.2", features = ["keccak"] } fluentbase-sdk-testing = { path = "../crates/testing" } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } diff --git a/examples/storage-usage/Cargo.toml b/examples/storage-usage/Cargo.toml new file mode 100644 index 000000000..95e1721e5 --- /dev/null +++ b/examples/storage-usage/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "fluentbase-examples-storage-sol" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +readme.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true + +[dependencies] +fluentbase-sdk = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } + +[dev-dependencies] +fluentbase-sdk = { workspace = true } +fluentbase-sdk-testing = { workspace = true } + +[features] +default = ["std"] +std = ["fluentbase-sdk/std"] + +[profile.dev] +debug = true + +[profile.test] +debug = true diff --git a/examples/storage-usage/README.md b/examples/storage-usage/README.md new file mode 100644 index 000000000..a89c4fdfd --- /dev/null +++ b/examples/storage-usage/README.md @@ -0,0 +1,3 @@ +# Storage + +An example of the storage usage. diff --git a/examples/storage-usage/src/lib.rs b/examples/storage-usage/src/lib.rs new file mode 100644 index 000000000..526041ca2 --- /dev/null +++ b/examples/storage-usage/src/lib.rs @@ -0,0 +1,3 @@ +mod nested; +mod simple; +mod utils; diff --git a/examples/storage-usage/src/nested.rs b/examples/storage-usage/src/nested.rs new file mode 100644 index 000000000..81d921bf2 --- /dev/null +++ b/examples/storage-usage/src/nested.rs @@ -0,0 +1,273 @@ +#![allow(dead_code)] +use fluentbase_sdk::{ + derive::Storage, + storage::{ + array::StorageArray, + composite::{Composite}, + map::StorageMap, + primitive::StoragePrimitive, + vec::StorageVec, + ArrayAccess, MapAccess, PrimitiveAccess, VecAccess, + }, + Address, SharedAPI, U256, +}; + +// Storage structures +#[derive(Storage)] +pub struct Item { + owner: StoragePrimitive
, + value: StoragePrimitive, + level: StoragePrimitive, + active: StoragePrimitive, +} + +#[derive(Storage)] +pub struct Inventory { + equipped_items: StorageArray, 3>, + user_items: StorageMap>, + collected_items: StorageVec>, + total_value: StoragePrimitive, + item_count: StoragePrimitive, +} + +#[derive(Storage)] +pub struct Game { + sdk: SDK, + admin: StoragePrimitive
, + version: StoragePrimitive, + player_inventory: Composite, + is_active: StoragePrimitive, +} + +// Data structures for passing values +#[derive(Clone, Debug, PartialEq)] +pub struct ItemData { + pub owner: Address, + pub value: U256, + pub level: u8, + pub active: bool, +} +#[derive(Clone, Debug, PartialEq)] +pub struct InventoryData { + pub equipped_items: [ItemData; 3], + pub user_items: Vec<(Address, ItemData)>, + pub collected_items: Vec, + pub total_value: U256, + pub item_count: u32, +} + +// Helper methods for Item +impl Item { + fn set_from(&self, data: &ItemData, sdk: &mut SDK) { + self.owner().set(sdk, data.owner); + self.value().set(sdk, data.value); + self.level().set(sdk, data.level); + self.active().set(sdk, data.active); + } + + fn get_data(&self, sdk: &SDK) -> ItemData { + ItemData { + owner: self.owner().get(sdk), + value: self.value().get(sdk), + level: self.level().get(sdk), + active: self.active().get(sdk), + } + } +} + +// Helper methods for Inventory +impl Inventory { + fn set_from(&self, data: &InventoryData, sdk: &mut SDK) { + // Set equipped items + for (i, item_data) in data.equipped_items.iter().enumerate() { + self.equipped_items.at(i).set_from(item_data, sdk); + } + + // Set user items + for (user, item_data) in &data.user_items { + self.user_items.entry(*user).set_from(item_data, sdk); + } + + // Set collected items + for item_data in &data.collected_items { + self.collected_items.push(sdk).set_from(item_data, sdk); + } + + // Set simple fields + self.total_value().set(sdk, data.total_value); + self.item_count().set(sdk, data.item_count); + } +} + +// Public API methods +impl Game { + // Simple setters + pub fn set_admin(&mut self, admin: Address) { + self.admin().set(&mut self.sdk, admin); + } + + pub fn set_version(&mut self, version: u32) { + self.version().set(&mut self.sdk, version); + } + + pub fn set_is_active(&mut self, active: bool) { + self.is_active().set(&mut self.sdk, active); + } + + // Inventory methods + pub fn set_inventory(&mut self, data: &InventoryData) { + self.player_inventory().set_from(data, &mut self.sdk); + } + + pub fn set_equipped_item(&mut self, index: usize, item: &ItemData) { + self.player_inventory() + .equipped_items + .at(index) + .set_from(item, &mut self.sdk); + } + + pub fn set_user_item(&mut self, user: Address, item: &ItemData) { + self.player_inventory() + .user_items + .entry(user) + .set_from(item, &mut self.sdk); + } + + pub fn add_collected_item(&mut self, item: &ItemData) { + let item_descriptor = self.player_inventory().collected_items.push(&mut self.sdk); + + item_descriptor.set_from(item, &mut self.sdk); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assert_storage_layout; + use crate::nested::{Game, Inventory, Item}; + use crate::utils::storage_from_fixture; + use fluentbase_sdk::address; + use fluentbase_sdk_testing::HostTestingContext; + + #[test] + fn test_layout_calculations() { + assert_storage_layout! { + Item => { + owner: 0, 12, + value: 1, 0, + level: 2, 31, + active: 2, 30, + }, + total_slots: 3 + } + + assert_storage_layout! { + Inventory => { + equipped_items: 0, 0, + user_items: 9, 0, + collected_items: 10, 0, + total_value: 11, 0, + item_count: 12, 28, + }, + total_slots: 13 + } + // assert_eq!(Game::::REQUIRED_SLOTS, 15); + } + + const EXPECTED_LAYOUT: &str = r#"{ + "0x0000000000000000000000000000000000000000": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x00000000000000000000002a1111111111111111111111111111111111111111", + "0x0000000000000000000000000000000000000000000000000000000000000001": "0x0000000000000000000000003333333333333333333333333333333333333333", + "0x0000000000000000000000000000000000000000000000000000000000000002": "0x0000000000000000000000000000000000000000000000000000000000000064", + "0x0000000000000000000000000000000000000000000000000000000000000003": "0x0000000000000000000000000000000000000000000000000000000000000101", + "0x0000000000000000000000000000000000000000000000000000000000000004": "0x0000000000000000000000003333333333333333333333333333333333333334", + "0x0000000000000000000000000000000000000000000000000000000000000005": "0x00000000000000000000000000000000000000000000000000000000000000c8", + "0x0000000000000000000000000000000000000000000000000000000000000006": "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000000000000000000000000000007": "0x0000000000000000000000003333333333333333333333333333333333333335", + "0x0000000000000000000000000000000000000000000000000000000000000008": "0x000000000000000000000000000000000000000000000000000000000000012c", + "0x0000000000000000000000000000000000000000000000000000000000000009": "0x0000000000000000000000000000000000000000000000000000000000000103", + "0x000000000000000000000000000000000000000000000000000000000000000b": "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x000000000000000000000000000000000000000000000000000000000000000c": "0x0000000000000000000000000000000000000000000000000000000000002710", + "0x000000000000000000000000000000000000000000000000000000000000000d": "0x0000000000000000000000000000000000000000000000000000000000000019", + "0x000000000000000000000000000000000000000000000000000000000000000e": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9": "0x0000000000000000000000004444444444444444444444444444444444444444", + "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dba": "0x00000000000000000000000000000000000000000000000000000000000001f4", + "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbb": "0x0000000000000000000000000000000000000000000000000000000000000105", + "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbc": "0x0000000000000000000000005555555555555555555555555555555555555555", + "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbd": "0x0000000000000000000000000000000000000000000000000000000000000258", + "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01dbe": "0x0000000000000000000000000000000000000000000000000000000000000006", + "0xc92a26ffa01eee8c1ebff84f57469c156727e09aacd6f4b34ca3f2083ce10698": "0x0000000000000000000000002222222222222222222222222222222222222222", + "0xc92a26ffa01eee8c1ebff84f57469c156727e09aacd6f4b34ca3f2083ce10699": "0x00000000000000000000000000000000000000000000000000000000000003e7", + "0xc92a26ffa01eee8c1ebff84f57469c156727e09aacd6f4b34ca3f2083ce1069a": "0x000000000000000000000000000000000000000000000000000000000000010a" + } +} +"#; + + #[test] + fn test_storage_layout_with_data_structures() { + let sdk = HostTestingContext::default(); + + let mut game = Game::new(sdk, U256::from(0), 0); + + // Set simple fields + game.set_admin(address!("0x1111111111111111111111111111111111111111")); + game.set_version(42); + game.set_is_active(true); + + let inventory_data = InventoryData { + equipped_items: [ + ItemData { + owner: address!("0x3333333333333333333333333333333333333333"), + value: U256::from(100), + level: 1, + active: true, + }, + ItemData { + owner: address!("0x3333333333333333333333333333333333333334"), + value: U256::from(200), + level: 2, + active: false, + }, + ItemData { + owner: address!("0x3333333333333333333333333333333333333335"), + value: U256::from(300), + level: 3, + active: true, + }, + ], + user_items: vec![( + address!("0x2222222222222222222222222222222222222222"), + ItemData { + owner: address!("0x2222222222222222222222222222222222222222"), + value: U256::from(999), + level: 10, + active: true, + }, + )], + collected_items: vec![ + ItemData { + owner: address!("0x4444444444444444444444444444444444444444"), + value: U256::from(500), + level: 5, + active: true, + }, + ItemData { + owner: address!("0x5555555555555555555555555555555555555555"), + value: U256::from(600), + level: 6, + active: false, + }, + ], + total_value: U256::from(10000), + item_count: 25, + }; + game.set_inventory(&inventory_data); + let storage = game.sdk.dump_storage(); + + // print resulting storage + // println!("{}", format_storage(&storage)); + + let expected_storage = storage_from_fixture(EXPECTED_LAYOUT); + assert_eq!(expected_storage, storage); + } +} diff --git a/examples/storage-usage/src/simple.rs b/examples/storage-usage/src/simple.rs new file mode 100644 index 000000000..21ecc3f26 --- /dev/null +++ b/examples/storage-usage/src/simple.rs @@ -0,0 +1,244 @@ +#![allow(dead_code)] +use fluentbase_sdk::{ + derive::Storage, + storage::{ + bytes::StorageString, composite::Composite, map::StorageMap, primitive::StoragePrimitive, + vec::StorageVec, MapAccess, PrimitiveAccess, VecAccess, + }, + Address, SharedAPI, U256, +}; + +// Storage structures +#[derive(Storage)] +pub struct State { + owner: StoragePrimitive
, + counter: StoragePrimitive, + balances: StorageMap>>, + data: StorageVec>, + name: StorageString, + description: StorageString, + is_active: StoragePrimitive, + is_paused: StoragePrimitive, + is_locked: StoragePrimitive, + version: StoragePrimitive, + flags: StoragePrimitive, +} + +#[derive(Storage)] +pub struct App { + sdk: SDK, + state: Composite, +} + +// Data structures for passing values +#[derive(Clone, Debug, PartialEq)] +pub struct StateData { + pub owner: Address, + pub counter: U256, + pub balances: Vec<(Address, Address, U256)>, // (owner, token, amount) + pub data_elements: Vec, + pub name: String, + pub description: String, + pub is_active: bool, + pub is_paused: bool, + pub is_locked: bool, + pub version: u32, + pub flags: u32, +} + +// Helper methods for State +impl State { + fn set_from(&self, data: &StateData, sdk: &mut SDK) { + self.owner().set(sdk, data.owner); + self.counter().set(sdk, data.counter); + + // Set balances + for (owner, token, amount) in &data.balances { + self.balances.entry(*owner).entry(*token).set(sdk, *amount); + } + + // Set vector elements + for element in &data.data_elements { + self.data.push(sdk).set(sdk, *element); + } + + // Set strings + self.name.set_string(sdk, &data.name); + self.description.set_string(sdk, &data.description); + + // Set packed fields + self.is_active().set(sdk, data.is_active); + self.is_paused().set(sdk, data.is_paused); + self.is_locked().set(sdk, data.is_locked); + self.version().set(sdk, data.version); + self.flags().set(sdk, data.flags); + } + + fn get_data(&self, sdk: &SDK) -> StateData { + // Get vector elements + let mut data_elements = Vec::new(); + let vec_len = self.data.len(sdk); + for i in 0..vec_len { + data_elements.push(self.data.at(i).get(sdk)); + } + + StateData { + owner: self.owner().get(sdk), + counter: self.counter().get(sdk), + balances: vec![], // Would need to track which keys were set + data_elements, + name: self.name.get_string(sdk), + description: self.description.get_string(sdk), + is_active: self.is_active().get(sdk), + is_paused: self.is_paused().get(sdk), + is_locked: self.is_locked().get(sdk), + version: self.version().get(sdk), + flags: self.flags().get(sdk), + } + } +} + +// Public API methods +impl App { + pub fn set_state(&mut self, data: &StateData) { + self.state().set_from(data, &mut self.sdk); + } + + pub fn get_state(&self) -> StateData { + self.state().get_data(&self.sdk) + } + + // Individual setters for convenience + pub fn set_owner(&mut self, owner: Address) { + self.state().owner().set(&mut self.sdk, owner); + } + + pub fn set_counter(&mut self, counter: U256) { + self.state().counter().set(&mut self.sdk, counter); + } + + pub fn set_balance(&mut self, owner: Address, token: Address, amount: U256) { + self.state() + .balances + .entry(owner) + .entry(token) + .set(&mut self.sdk, amount); + } + + pub fn get_balance(&self, owner: Address, token: Address) -> U256 { + self.state() + .balances + .entry(owner) + .entry(token) + .get(&self.sdk) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::assert_storage_layout; + use crate::utils::storage_from_fixture; + use fluentbase_sdk::address; + use fluentbase_sdk_testing::HostTestingContext; + + #[test] + fn test_layout_calculations() { + assert_storage_layout! { + State => { + owner: 0, 12, + counter: 1, 0, + balances: 2, 0, + data: 3, 0, + name: 4, 0, + description: 5, 0, + is_active: 6, 31, + is_paused: 6, 30, + is_locked: 6, 29, + version: 6, 25, + flags: 6, 21, + }, + total_slots: 7 + } + } + + const EXPECTED_LAYOUT: &str = r#"{ + "0x0000000000000000000000000000000000000000": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000001111111111111111111111111111111111111111", + "0x0000000000000000000000000000000000000000000000000000000000000001": "0x000000000000000000000000000000000000000000000000000000000000002a", + "0x0000000000000000000000000000000000000000000000000000000000000003": "0x0000000000000000000000000000000000000000000000000000000000000003", + "0x0000000000000000000000000000000000000000000000000000000000000004": "0x7465737400000000000000000000000000000000000000000000000000000008", + "0x0000000000000000000000000000000000000000000000000000000000000005": "0x00000000000000000000000000000000000000000000000000000000000000b3", + "0x0000000000000000000000000000000000000000000000000000000000000006": "0x000000000000000000000000000000000000000000deadbeef00003039010001", + "0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db0": "0x7468697320697320612076657279206c6f6e67206465736372697074696f6e20", + "0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db1": "0x74686174206578636565647320333120627974657320616e642073686f756c64", + "0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db2": "0x2062652073746f726564206173206c6f6e6720737472696e6700000000000000", + "0xb34395caba8110d7cdff9a58077ab0a87eb0ab00b5e8dff5b3370b0e6a6b7744": "0x00000000000000000000000000000000000000000000000000000000000003e8", + "0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b": "0x000000000000000000000000000000000000000000000000000000000000006f", + "0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85c": "0x00000000000000000000000000000000000000000000000000000000000000de", + "0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85d": "0x000000000000000000000000000000000000000000000000000000000000014d" + } +}"#; + + #[test] + fn test_storage_layout_with_data() { + let sdk = HostTestingContext::default(); + let mut app = App::new(sdk, U256::from(0), 0); + + let state_data = StateData { + owner: address!("0x1111111111111111111111111111111111111111"), + counter: U256::from(42), + balances: vec![( + address!("0x2222222222222222222222222222222222222222"), + address!("0x3333333333333333333333333333333333333333"), + U256::from(1000), + )], + data_elements: vec![U256::from(111), U256::from(222), U256::from(333)], + name: "test".to_string(), + description: "this is a very long description that exceeds 31 bytes and should be stored as long string".to_string(), + is_active: true, + is_paused: false, + is_locked: true, + version: 12345, + flags: 0xDEADBEEF, + }; + + // Set all state at once + app.set_state(&state_data); + + // Verify we can read it back correctly + let retrieved = app.get_state(); + assert_eq!(retrieved.owner, state_data.owner); + assert_eq!(retrieved.counter, state_data.counter); + assert_eq!(retrieved.data_elements, state_data.data_elements); + assert_eq!(retrieved.name, state_data.name); + assert_eq!(retrieved.description, state_data.description); + assert_eq!(retrieved.is_active, state_data.is_active); + assert_eq!(retrieved.is_paused, state_data.is_paused); + assert_eq!(retrieved.is_locked, state_data.is_locked); + assert_eq!(retrieved.version, state_data.version); + assert_eq!(retrieved.flags, state_data.flags); + + // Verify balance through direct getter + assert_eq!( + app.get_balance( + address!("0x2222222222222222222222222222222222222222"), + address!("0x3333333333333333333333333333333333333333") + ), + U256::from(1000) + ); + + // Dump and compare storage + let storage = app.sdk.dump_storage(); + + // Uncomment to see actual storage layout + // println!("actual:\n {}", format_storage(&storage)); + + let expected_storage = storage_from_fixture(EXPECTED_LAYOUT); + + // Uncomment to see expected storage layout + // println!("expected:\n {}", format_storage(&expected_storage)); + + assert_eq!(expected_storage, storage); + } +} diff --git a/examples/storage-usage/src/utils.rs b/examples/storage-usage/src/utils.rs new file mode 100644 index 000000000..5fa74e30d --- /dev/null +++ b/examples/storage-usage/src/utils.rs @@ -0,0 +1,99 @@ +#![allow(dead_code)] +#![cfg(test)] +use fluentbase_sdk::{Address, HashMap, U256}; + +#[macro_export] +macro_rules! assert_storage_layout { + ( + $struct_type:ty => { + $( + $field:ident: $slot:expr, $offset:expr + ),* $(,)? + }, + total_slots: $total_slots:expr + ) => { + { + use fluentbase_sdk::storage::{composite::CompositeStorage, StorageDescriptor}; + use fluentbase_sdk::U256; + + let instance = <$struct_type>::new(U256::from(0), 0); + + $( + assert_eq!(instance.$field.slot(), U256::from($slot)); + assert_eq!(instance.$field.offset(), $offset); + )* + + assert_eq!( + <$struct_type as CompositeStorage>::REQUIRED_SLOTS, + $total_slots, + "Total slots mismatch: expected {}, got {}", + $total_slots, <$struct_type as CompositeStorage>::REQUIRED_SLOTS + ); + } + }; +} + +/// Creates a MockStorage instance from a JSON fixture +/// +/// Expected JSON format: +/// ```json +/// { +/// "expected_storage": { +/// "0x0000...0000": "0xabcd...ef12", +/// "0x0000...0001": "0x1234...5678" +/// } +/// } +/// ``` + +pub(crate) fn storage_from_fixture(json: &str) -> HashMap<(Address, U256), U256> { + use core::str::FromStr; + use serde_json::Value; + + let fixture: Value = serde_json::from_str(json).expect("Invalid JSON"); + let mut storage = HashMap::new(); + + if let Some(contracts) = fixture.as_object() { + for (address_str, slots) in contracts { + let address = Address::from_str(address_str).expect("Invalid address"); + + if let Some(slots_map) = slots.as_object() { + for (slot_str, value_str) in slots_map { + let slot = U256::from_str(slot_str).expect("Invalid slot"); + let value = U256::from_str(value_str.as_str().expect("Value must be string")) + .expect("Invalid value"); + + storage.insert((address, slot), value); + } + } + } + } + + storage +} + +/// Pretty-prints storage entries grouped by contract address and sorted by slot. +pub(crate) fn format_storage(storage: &HashMap<(Address, U256), U256>) -> String { + if storage.is_empty() { + return " (empty)".to_string(); + } + + let mut entries: Vec<_> = storage.iter().collect(); + entries.sort_by_key(|((addr, slot), _)| (*addr, *slot)); + + let mut result = String::new(); + let mut current_addr: Option
= None; + + for ((addr, slot), value) in entries { + if current_addr != Some(*addr) { + if current_addr.is_some() { + result.push('\n'); + } + result.push_str(&format!(" Contract 0x{:040x}:\n", addr)); + current_addr = Some(*addr); + } + + result.push_str(&format!(" Slot 0x{:064x}: 0x{:064x}\n", slot, value)); + } + + result +} diff --git a/examples/storage/Cargo.toml b/examples/storage/Cargo.toml index 36a4a52b1..2dbb6f387 100644 --- a/examples/storage/Cargo.toml +++ b/examples/storage/Cargo.toml @@ -11,6 +11,8 @@ categories.workspace = true [dependencies] fluentbase-sdk = { workspace = true } +alloy-sol-types = { version = "1.3.0", default-features = false, features = ["json"]} +hex-literal = "1.0.0" [dev-dependencies] serial_test = "3.0.0" diff --git a/examples/storage/lib.rs b/examples/storage/lib.rs index f8b0d55e6..9ab644042 100644 --- a/examples/storage/lib.rs +++ b/examples/storage/lib.rs @@ -1,255 +1,319 @@ -#![cfg_attr(target_arch = "wasm32", no_std, no_main)] -use fluentbase_sdk::{ - codec::Codec, - derive::solidity_storage, - entrypoint, - Address, - Bytes, - FixedBytes, - SharedAPI, - I256, - U256, -}; - -#[derive(Codec, Debug, Default, Clone, PartialEq)] -pub struct MyStruct { - pub a: U256, - pub b: U256, - pub c: Bytes, - pub d: Bytes, -} - -solidity_storage! { - // For this types DirectStorage is used (it's allows to reduce code size by avoiding redundant encoding/decoding) - FixedBytes<32> CustomFixedBytes; - [u8; 32] CustomFixedBytesArray; - U256 CustomU256; - I256 CustomI256; - u64 CustomU64; - i64 CustomI64; - - // For this types SolidityStorage is used (it's allows to use dynamic types like Vec or String) - mapping(Address => U256) Balance; - mapping(Address => mapping(Address => U256)) Allowance; - U256[] Arr; - Address[][][] NestedArr; - Address Owner; - Bytes Data; - MyStruct SomeStruct; - mapping(Address => MyStruct) MyStructMap; -} - -pub fn main_entry(sdk: impl SharedAPI) { - sdk.exit(); -} - -entrypoint!(main_entry); - -#[cfg(test)] -mod test { - use super::*; - use fluentbase_sdk::{hex, ContractContextV1}; - use fluentbase_sdk_testing::HostTestingContext; - use serial_test::serial; - - fn with_test_input>(input: T, caller: Option
) -> HostTestingContext { - HostTestingContext::default() - .with_contract_context(ContractContextV1 { - caller: caller.unwrap_or_default(), - ..Default::default() - }) - .with_devnet_genesis() - .with_input(input) - } - - #[serial] - #[test] - pub fn test_primitive_storage_dynamic_bytes() { - let mut sdk = with_test_input(vec![], None); - let b = fluentbase_sdk::Bytes::from( - "this is a really long string. this is a really long - string. this is a really long string. this it really long string.", - ); - Data::set(&mut sdk, b.clone()); - let result = Data::get(&sdk); - assert_eq!(result, b); - } - - #[serial] - #[test] - pub fn test_nested_arr() { - let mut sdk = with_test_input(vec![], None); - let idx1 = U256::from(0); - let idx2 = U256::from(0); - let idx3 = U256::from(0); - let value = Address::from(hex!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")); - NestedArr::set(&mut sdk, idx1, idx2, idx3, value); - let result = NestedArr::get(&sdk, idx1, idx2, idx3); - assert_eq!(result, value); - } - - #[serial] - #[test] - pub fn test_storage_mapping() { - let mut sdk = with_test_input(vec![], None); - let addr = Address::from(hex!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")); - let value: U256 = U256::from_str_radix("1000000000000000000", 10).unwrap(); // 1000 - Balance::set(&mut sdk, addr, value); - let result = Balance::get(&sdk, addr); - assert_eq!(result, value); - } - - #[serial] - #[test] - pub fn test_storage_mapping_nested() { - let mut sdk = with_test_input(vec![], None); - let addr1 = Address::from(hex!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")); - let addr2 = Address::from(hex!("70997970C51812dc3A010C7d01b50e0d17dc79C8")); - let value: U256 = U256::from_str_radix("1000000000000000000", 10).unwrap(); - Allowance::set(&mut sdk, addr1, addr2, value); - let result = Allowance::get(&sdk, addr1, addr2); - assert_eq!(result, result); - } - - #[serial] - #[test] - pub fn test_storage_primitive_address() { - let mut sdk = with_test_input(vec![], None); - let addr1 = Address::from(hex!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")); - Owner::set(&mut sdk, addr1); - let result = Owner::get(&sdk); - assert_eq!(result, result); - } - - #[serial] - #[test] - pub fn test_storage_primitive_u256() { - let mut sdk = with_test_input(vec![], None); - let a = U256::from(1); - CustomU256::set(&mut sdk, a.clone()); - let result = CustomU256::get(&sdk); - assert_eq!(result, a); - } - - #[serial] - #[test] - pub fn test_storage_primitive_i256() { - let mut sdk = with_test_input(vec![], None); - let a = I256::try_from(I256::MIN).unwrap(); - CustomI256::set(&mut sdk, a.clone()); - let result = CustomI256::get(&sdk); - assert_eq!(result, a); - } - - #[serial] - #[test] - pub fn test_storage_primitive_struct() { - let mut sdk = with_test_input(vec![], None); - let a = U256::from(1); - let b = U256::from(2); - let c = fluentbase_sdk::Bytes::from( - "this it really long string. this it really long - string. this it really long string. this it really long string.", - ); - let d = fluentbase_sdk::Bytes::from("short"); - let my_struct = MyStruct { a, b, c, d }; - SomeStruct::set(&mut sdk, my_struct.clone()); - let result = SomeStruct::get(&sdk); - assert_eq!(result, my_struct); - } - - #[serial] - #[test] - pub fn test_storage_mapping_struct() { - let mut sdk = with_test_input(vec![], None); - let addr = Address::from(hex!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")); - let a = U256::from(1); - let b = U256::from(2); - let c = fluentbase_sdk::Bytes::from( - "this it really long string. this it really long - string. this it really long string. this it really long string.", - ); - let d = fluentbase_sdk::Bytes::from("short"); - let my_struct = MyStruct { a, b, c, d }; - MyStructMap::set(&mut sdk, addr, my_struct.clone()); - let result = MyStructMap::get(&sdk, addr); - assert_eq!(result, my_struct.clone()); - } - - #[serial] - #[test] - pub fn test_storage_key_reference_values() { - let sdk = with_test_input(vec![], None); - - // Test slot values - assert_eq!(CustomFixedBytes::SLOT, U256::from_limbs([0, 0, 0, 0])); - assert_eq!(CustomFixedBytesArray::SLOT, U256::from_limbs([1, 0, 0, 0])); - assert_eq!(Balance::SLOT, U256::from_limbs([6, 0, 0, 0])); - assert_eq!(Allowance::SLOT, U256::from_limbs([7, 0, 0, 0])); - assert_eq!(Owner::SLOT, U256::from_limbs([10, 0, 0, 0])); - - let addr1 = Address::from(hex!("f39fd6e51aad88f6f4ce6ab8827279cfffb92266")); - let addr2 = Address::from(hex!("70997970c51812dc3a010c7d01b50e0d17dc79c8")); - - // Balance[addr1] - // Expected format: keccak256(abi.encodePacked(bytes32(address), bytes32(slot))) - let expected_balance_key = U256::from_str_radix( - "c50c4d60f8bbb6a70920d195c8852bc6d816d9f7bc643b500261fc4d9a03f08c", - 16, - ) - .unwrap(); - let actual_balance_key = Balance::key(&sdk, addr1); - assert_eq!( - actual_balance_key, expected_balance_key, - "Balance key mismatch: expected {:?}, got {:?}", - expected_balance_key, actual_balance_key - ); - - // Allowance[addr1][addr2] - // Expected format: keccak256(abi.encodePacked(bytes32(addr2), - // keccak256(abi.encodePacked(bytes32(addr1), bytes32(slot))))) - let expected_allowance_key = U256::from_str_radix( - "9497c69828ddf28f6ad649ddac9a7c28d7e9228a5a06a6acf21099fe94d38327", - 16, - ) - .unwrap(); - let actual_allowance_key = Allowance::key(&sdk, addr1, addr2); - assert_eq!( - actual_allowance_key, expected_allowance_key, - "Allowance key mismatch: expected {:?}, got {:?}", - expected_allowance_key, actual_allowance_key - ); - - // Arr[42] - // Expected format: keccak256(abi.encodePacked(bytes32(slot))) + index - let idx = U256::from(42); - let expected_array_key = U256::from_str_radix( - "f3f7a9fe364faab93b216da50a3214154f22a0a2b415b23a84c8169e8b636f0d", - 16, - ) - .unwrap(); - let actual_array_key = Arr::key(&sdk, idx); - assert_eq!( - actual_array_key, expected_array_key, - "Array key mismatch: expected {:?}, got {:?}", - expected_array_key, actual_array_key - ); - - // NestedArr[0][0][0] - // Complex nested array calculation - let idx1 = U256::from(0); - let idx2 = U256::from(0); - let idx3 = U256::from(0); - let expected_nested_key = U256::from_str_radix( - "7e36832397f38490551808dffc4af389da37450fee8ca1202b2419425bcdb132", - 16, - ) - .unwrap(); - let actual_nested_key = NestedArr::key(&sdk, idx1, idx2, idx3); - assert_eq!( - actual_nested_key, expected_nested_key, - "NestedArr key mismatch: expected {:?}, got {:?}", - expected_nested_key, actual_nested_key - ); - } -} +// #![cfg_attr(not(feature = "std"), no_std, no_main)] +// #![allow(dead_code)] +// extern crate alloc; +// extern crate fluentbase_sdk; + +// use alloc::{string::String, vec::Vec}; +// use alloy_sol_types::{sol, SolEvent}; +// use fluentbase_sdk::{ +// basic_entrypoint, +// codec::Codec, +// derive::{router, solidity_storage, Contract}, +// Address, ContextReader, SharedAPI, B256, U256, +// }; + +// pub trait ERC20API { +// fn constructor(&mut self, name: String, symbol: String, total_supply: U256); +// fn symbol(&self) -> String; +// fn name(&self) -> String; +// fn decimals(&self) -> U256; +// fn total_supply(&self) -> U256; +// fn balance_of(&self, account: Address) -> U256; +// fn transfer(&mut self, to: Address, value: U256) -> U256; +// fn allowance(&self, owner: Address, spender: Address) -> U256; +// fn approve(&mut self, spender: Address, value: U256) -> U256; +// fn transfer_from(&mut self, from: Address, to: Address, value: U256) -> U256; +// } + +// #[derive(Codec, Debug, Clone)] +// struct ERC20ConstructorArgs { +// name: String, +// symbol: String, +// initial_supply: U256, +// } + +// // Define the Transfer and Approval events +// sol! { +// event Transfer(address indexed from, address indexed to, uint256 value); +// event Approval(address indexed owner, address indexed spender, uint256 value); +// } + +// fn emit_event(sdk: &mut SDK, event: T) { +// let data = event.encode_data(); +// let topics: Vec = event +// .encode_topics() +// .iter() +// .map(|v| B256::from(v.0)) +// .collect(); +// sdk.emit_log(&topics, &data); +// } + +// solidity_storage! { +// U256 InitialSupply; +// String TokenName; +// String TokenSymbol; +// mapping(Address => U256) Balance; +// mapping(Address => mapping(Address => U256)) Allowance; +// } + +// impl Balance { +// fn add( +// sdk: &mut SDK, +// address: Address, +// amount: U256, +// ) -> Result<(), &'static str> { +// let current_balance = Self::get(sdk, address); +// let new_balance = current_balance + amount; +// Self::set(sdk, address, new_balance); +// Ok(()) +// } +// fn subtract( +// sdk: &mut SDK, +// address: Address, +// amount: U256, +// ) -> Result<(), &'static str> { +// let current_balance = Self::get(sdk, address); +// if current_balance < amount { +// return Err("insufficient balance"); +// } +// let new_balance = current_balance - amount; +// Self::set(sdk, address, new_balance); +// Ok(()) +// } +// } + +// impl Allowance { +// fn add( +// sdk: &mut SDK, +// owner: Address, +// spender: Address, +// amount: U256, +// ) -> Result<(), &'static str> { +// let current_allowance = Self::get(sdk, owner, spender); +// let new_allowance = current_allowance + amount; +// Self::set(sdk, owner, spender, new_allowance); +// Ok(()) +// } +// fn subtract( +// sdk: &mut SDK, +// owner: Address, +// spender: Address, +// amount: U256, +// ) -> Result<(), &'static str> { +// let current_allowance = Self::get(sdk, owner, spender); +// if current_allowance < amount { +// return Err("insufficient allowance"); +// } +// let new_allowance = current_allowance - amount; +// Self::set(sdk, owner, spender, new_allowance); +// Ok(()) +// } +// } + +// #[derive(Contract, Default)] +// struct ERC20 { +// sdk: SDK, +// } + +// #[router(mode = "solidity")] +// impl ERC20API for ERC20 { +// fn constructor(&mut self, name: String, symbol: String, supply: U256) { +// TokenSymbol::set(&mut self.sdk, symbol); +// TokenName::set(&mut self.sdk, name); +// InitialSupply::set(&mut self.sdk, supply); +// let owner_address = self.sdk.context().contract_caller(); +// let _ = Balance::add(&mut self.sdk, owner_address, supply); +// } + +// fn symbol(&self) -> String { +// TokenSymbol::get(&self.sdk) +// } + +// fn name(&self) -> String { +// TokenName::get(&self.sdk) +// } + +// fn decimals(&self) -> U256 { +// U256::from(18) +// } + +// fn total_supply(&self) -> U256 { +// InitialSupply::get(&self.sdk) +// } + +// fn balance_of(&self, account: Address) -> U256 { +// Balance::get(&self.sdk, account) +// } + +// fn transfer(&mut self, to: Address, value: U256) -> U256 { +// let from = self.sdk.context().contract_caller(); + +// Balance::subtract(&mut self.sdk, from, value).unwrap_or_else(|err| panic!("{}", err)); +// Balance::add(&mut self.sdk, to, value).unwrap_or_else(|err| panic!("{}", err)); + +// emit_event(&mut self.sdk, Transfer { from, to, value }); +// U256::from(1) +// } + +// fn allowance(&self, owner: Address, spender: Address) -> U256 { +// Allowance::get(&self.sdk, owner, spender) +// } + +// fn approve(&mut self, spender: Address, value: U256) -> U256 { +// let owner = self.sdk.context().contract_caller(); +// Allowance::set(&mut self.sdk, owner, spender, value); +// emit_event( +// &mut self.sdk, +// Approval { +// owner, +// spender, +// value, +// }, +// ); +// U256::from(1) +// } + +// fn transfer_from(&mut self, from: Address, to: Address, value: U256) -> U256 { +// let spender = self.sdk.context().contract_caller(); + +// let current_allowance = Allowance::get(&self.sdk, from, spender); +// if current_allowance < value { +// panic!("insufficient allowance"); +// } + +// Allowance::subtract(&mut self.sdk, from, spender, value) +// .unwrap_or_else(|err| panic!("{}", err)); +// Balance::subtract(&mut self.sdk, from, value).unwrap_or_else(|err| panic!("{}", err)); +// Balance::add(&mut self.sdk, to, value).unwrap_or_else(|err| panic!("{}", err)); + +// emit_event(&mut self.sdk, Transfer { from, to, value }); +// U256::from(1) +// } +// } + +// basic_entrypoint!(ERC20); + +// #[cfg(test)] +// mod tests { +// use std::io::Read; + +// use super::*; +// use fluentbase_sdk::{address, bytes::BytesMut, hex, ContractContextV1, U256}; +// use fluentbase_sdk::codec::SolidityABI; +// use fluentbase_sdk_testing::HostTestingContext; + +// #[test] +// fn test_erc20_deploy_constructor_args() { +// let mut buf = BytesMut::new(); +// let args: (String, String, U256) = +// ("MyToken".to_string(), "MTK".to_string(), U256::from(1_000_000u64)); + +// SolidityABI::encode(&args, &mut buf, 0).unwrap(); + +// let input = buf.freeze(); +// println!("input {:?}", hex::encode(&input)); + +// let context = ContractContextV1 { +// address: address!("1111111111111111111111111111111111111111"), +// bytecode_address: address!("2222222222222222222222222222222222222222"), +// caller: address!("3333333333333333333333333333333333333333"), +// is_static: false, +// value: U256::ZERO, +// gas_limit: 0, +// }; + +// let sdk = +// HostTestingContext::default().with_input(input).with_contract_context(context.clone()); + +// let mut contract = ERC20 { sdk: sdk.clone() }; +// contract.deploy(); +// let storage_symbol = TokenSymbol::get(&mut contract.sdk); +// println!("storage symbol {:?}", storage_symbol); +// } +// } +// // +// // #[cfg(test)] +// // mod tests { +// // use super::*; +// // use crate::assert_storage_layout; +// // use fluentbase_sdk::address; +// // use fluentbase_sdk_testing::HostTestingContext; +// // +// // #[test] +// // fn test_layout_calculations() { +// // assert_storage_layout! { +// // TokenInfo => { +// // name: 0, 0, +// // symbol: 1, 0, +// // decimals: 2, 31, +// // total_supply: 3, 0, +// // }, +// // total_slots: 4 +// // } +// // +// // assert_storage_layout! { +// // ERC20State => { +// // token_info: 0, 0, +// // balances: 4, 0, +// // allowances: 5, 0, +// // }, +// // total_slots: 6 +// // } +// // } +// // +// // #[test] +// // fn test_erc20_deployment() { +// // let context = ContractContextV1 { +// // address: address!("1111111111111111111111111111111111111111"), +// // caller: address!("3333333333333333333333333333333333333333"), +// // // ... other fields +// // }; +// // +// // let sdk = HostTestingContext::default().with_contract_context(context); +// // +// // let mut contract = ERC20::new(sdk, U256::from(0), 0); +// // +// // // Deploy with constructor +// // contract.constructor( +// // "MyToken".to_string(), +// // "MTK".to_string(), +// // U256::from(1_000_000), +// // ); +// // +// // // Verify token info +// // assert_eq!(contract.name(), "MyToken"); +// // assert_eq!(contract.symbol(), "MTK"); +// // assert_eq!(contract.decimals(), 18); +// // assert_eq!(contract.total_supply(), U256::from(1_000_000)); +// // +// // // Verify deployer balance +// // let deployer = address!("3333333333333333333333333333333333333333"); +// // assert_eq!(contract.balance_of(deployer), U256::from(1_000_000)); +// // } +// // +// // #[test] +// // fn test_erc20_transfer() { +// // let sdk = HostTestingContext::default(); +// // let mut contract = ERC20::new(sdk, U256::from(0), 0); +// // +// // contract.constructor("Test".to_string(), "TST".to_string(), U256::from(1000)); +// // +// // let alice = address!("1111111111111111111111111111111111111111"); +// // let bob = address!("2222222222222222222222222222222222222222"); +// // +// // // Setup: give Alice some tokens +// // contract +// // .state() +// // .balances +// // .entry(alice) +// // .set(&mut contract.sdk, U256::from(100)); +// // +// // // Transfer from Alice to Bob +// // contract.sdk = contract.sdk.with_caller(alice); +// // assert!(contract.transfer(bob, U256::from(30))); +// // +// // // Check balances +// // assert_eq!(contract.balance_of(alice), U256::from(70)); +// // assert_eq!(contract.balance_of(bob), U256::from(30)); +// // } +// // } From 197728a1c5bc7543531ad57e92daf3dcdde659c4 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Fri, 12 Sep 2025 13:15:53 +0400 Subject: [PATCH 08/16] feat(codec): add function args encoding without tuple offset --- crates/codec/src/encoder.rs | 36 +++++++- crates/codec/src/func.rs | 129 +++++++++++++++++++++++++++ crates/codec/src/lib.rs | 1 + crates/codec/src/tuple.rs | 8 ++ crates/codec/tests/roundtrip/func.rs | 93 +++++++++++++++++++ crates/codec/tests/roundtrip/mod.rs | 1 + 6 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 crates/codec/src/func.rs create mode 100644 crates/codec/tests/roundtrip/func.rs diff --git a/crates/codec/src/encoder.rs b/crates/codec/src/encoder.rs index a6f498a04..1252f3a17 100644 --- a/crates/codec/src/encoder.rs +++ b/crates/codec/src/encoder.rs @@ -1,8 +1,8 @@ +use crate::func::FunctionArgs; use crate::{alloc::string::ToString, error::CodecError}; use byteorder::{ByteOrder, BE, LE}; use bytes::{Buf, BytesMut}; use core::marker::PhantomData; - // TODO: @d1r1 Investigate whether decoding the result into an uninitialized memory (e.g., using // `MaybeUninit`) would be more efficient than initializing with `Default`. // This could potentially reduce unnecessary memory initialization overhead in cases where @@ -62,6 +62,7 @@ pub trait Encoder(Self::HEADER_SIZE) } } + macro_rules! define_encoder_mode { ($name:ident, $byte_order:ty, $align:expr, $sol_mode:expr) => { pub struct $name(PhantomData); @@ -133,6 +134,37 @@ define_encoder_mode!(CompactABI, LE, 4, false); // SolidityPackedABI works only for static types define_encoder_mode!(SolidityPackedABI, BE, 1, true, static_only); +impl SolidityABI { + pub fn encode_function_args(value: &T, buf: &mut BytesMut) -> Result<(), CodecError> + where + T: FunctionArgs, + { + value.encode_as_args(buf) + } + + pub fn decode_function_args(buf: &impl Buf) -> Result + where + T: FunctionArgs, + { + T::decode_as_args(buf) + } +} + +impl CompactABI { + pub fn encode_function_args(value: &T, buf: &mut BytesMut) -> Result<(), CodecError> + where + T: FunctionArgs, + { + value.encode_as_args(buf) + } + + pub fn decode_function_args(buf: &impl Buf) -> Result + where + T: FunctionArgs, + { + T::decode_as_args(buf) + } +} pub trait SolidityEncoder: Encoder { const SOLIDITY_HEADER_SIZE: usize = >::HEADER_SIZE; } @@ -241,7 +273,7 @@ pub(crate) fn get_aligned_slice( // For big-endian, return slice at the end of the aligned space aligned_offset + word_size - value_size } else { - // For little-endian, return slice at the beginning of the aligned space + // For little-endian, return a slice at the beginning of the aligned space aligned_offset }; diff --git a/crates/codec/src/func.rs b/crates/codec/src/func.rs new file mode 100644 index 000000000..de318bb93 --- /dev/null +++ b/crates/codec/src/func.rs @@ -0,0 +1,129 @@ +use crate::encoder::Encoder; +use crate::error::CodecError; +use byteorder::ByteOrder; +use bytes::{Buf, BytesMut}; + +/// Trait for types that can be used as function arguments +/// Only implemented for tuples +pub trait FunctionArgs< + B: ByteOrder, + const ALIGN: usize, + const SOL_MODE: bool, + const IS_STATIC: bool, +>: Encoder +{ + /// Encode without the outer tuple offset for dynamic types + fn encode_as_args(&self, buf: &mut BytesMut) -> Result<(), CodecError> { + if Self::IS_DYNAMIC { + // For dynamic types, skip the first ALIGN bytes (the tuple offset) + let mut temp_buf = BytesMut::new(); + self.encode(&mut temp_buf, 0)?; + + // Skip the offset and append the rest + if temp_buf.len() > ALIGN { + buf.extend_from_slice(&temp_buf[ALIGN..]); + } + Ok(()) + } else { + // Static types encode normally + self.encode(buf, buf.len()) + } + } + + /// Decode expecting no outer tuple offset for dynamic types + fn decode_as_args(buf: &impl Buf) -> Result { + if Self::IS_DYNAMIC { + use bytes::BufMut; + // Add the offset back for proper decoding + let mut temp_buf = BytesMut::with_capacity(ALIGN + buf.remaining()); + + // Add offset header + if SOL_MODE { + // Solidity: 32-byte aligned, big-endian + temp_buf.resize(ALIGN, 0); + let offset = (ALIGN as u32).to_be_bytes(); + temp_buf[ALIGN - 4..ALIGN].copy_from_slice(&offset); + } else { + // Fluent: 4-byte, little-endian + temp_buf.put_u32_le(ALIGN as u32); + } + + // Add the actual data + temp_buf.extend_from_slice(buf.chunk()); + + // Decode with the reconstructed offset + Self::decode(&temp_buf.freeze(), 0) + } else { + // Static types decode normally + Self::decode(buf, 0) + } + } +} + +// Implement for all tuples - super simple! +impl + FunctionArgs for () +{ +} + +impl + FunctionArgs for (T,) +where + T: Encoder, +{ +} + +// Macro for Solidity mode (SOL_MODE = true) +macro_rules! impl_function_args_solidity { + ($($T:ident),+) => { + impl<$($T,)+ B: ByteOrder, const ALIGN: usize, const IS_STATIC: bool> + FunctionArgs for ($($T,)+) + where + $($T: Encoder,)+ + {} + }; +} + +// Macro for Compact mode (SOL_MODE = false) +macro_rules! impl_function_args_compact { + ($($T:ident),+) => { + impl<$($T,)+ B: ByteOrder, const ALIGN: usize, const IS_STATIC: bool> + FunctionArgs for ($($T,)+) + where + $($T: Encoder,)+ + {} + }; +} + +impl_function_args_solidity!(T0, T1); +impl_function_args_compact!(T0, T1); + +impl_function_args_solidity!(T0, T1, T2); +impl_function_args_compact!(T0, T1, T2); + +impl_function_args_solidity!(T0, T1, T2, T3); +impl_function_args_compact!(T0, T1, T2, T3); + +impl_function_args_solidity!(T0, T1, T2, T3, T4); +impl_function_args_compact!(T0, T1, T2, T3, T4); + +impl_function_args_solidity!(T0, T1, T2, T3, T4, T5); +impl_function_args_compact!(T0, T1, T2, T3, T4, T5); + +impl_function_args_solidity!(T0, T1, T2, T3, T4, T5, T6); +impl_function_args_compact!(T0, T1, T2, T3, T4, T5, T6); + +impl_function_args_solidity!(T0, T1, T2, T3, T4, T5, T6, T7); +impl_function_args_compact!(T0, T1, T2, T3, T4, T5, T6, T7); + +impl_function_args_solidity!(T0, T1, T2, T3, T4, T5, T6, T7, T8); +impl_function_args_compact!(T0, T1, T2, T3, T4, T5, T6, T7, T8); + +impl_function_args_solidity!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9); +impl_function_args_compact!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9); + +impl_function_args_solidity!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); +impl_function_args_compact!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); + +impl_function_args_solidity!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); +impl_function_args_compact!(T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); diff --git a/crates/codec/src/lib.rs b/crates/codec/src/lib.rs index 2aa933eed..02afa57ad 100644 --- a/crates/codec/src/lib.rs +++ b/crates/codec/src/lib.rs @@ -8,6 +8,7 @@ mod empty; pub mod encoder; mod error; mod evm; +mod func; mod hash; mod primitive; mod tuple; diff --git a/crates/codec/src/tuple.rs b/crates/codec/src/tuple.rs index 3778548bd..0119e4395 100644 --- a/crates/codec/src/tuple.rs +++ b/crates/codec/src/tuple.rs @@ -269,6 +269,14 @@ impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7; 0, 1, 2, 3, 4, 5, 6; true); impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7; 0, 1, 2, 3, 4, 5, 6; false); impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8; 0, 1, 2, 3, 4, 5, 6, 7; true); impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8; 0, 1, 2, 3, 4, 5, 6, 7; false); +impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9; 0, 1, 2, 3, 4, 5, 6, 7, 8; true); +impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9; 0, 1, 2, 3, 4, 5, 6, 7, 8; false); +impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9; true); +impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9; false); +impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10; true); +impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10; false); +impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11; true); +impl_encoder_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11; false); #[cfg(test)] mod tests { diff --git a/crates/codec/tests/roundtrip/func.rs b/crates/codec/tests/roundtrip/func.rs new file mode 100644 index 000000000..92ad23d47 --- /dev/null +++ b/crates/codec/tests/roundtrip/func.rs @@ -0,0 +1,93 @@ +use super::*; + +#[test] +fn test_solidity_function_args_dynamic() { + // Test that dynamic tuple encoding skips the outer offset + let args = (Bytes::from("hello"), U256::from(42)); + + let mut buf_normal = BytesMut::new(); + let mut buf_func = BytesMut::new(); + + // Normal encoding (with tuple offset) + SolidityABI::encode(&args, &mut buf_normal, 0).unwrap(); + + // Function args encoding (without tuple offset) + SolidityABI::encode_function_args(&args, &mut buf_func).unwrap(); + + // Function args should be 32 bytes shorter (no tuple offset) + assert_eq!(buf_func.len(), buf_normal.len() - 32); + assert_eq!(&buf_func[..], &buf_normal[32..]); + + // Decode and verify + let decoded: (Bytes, U256) = SolidityABI::decode_function_args(&buf_func).unwrap(); + assert_eq!(decoded, args); +} + +#[test] +fn test_solidity_function_args_static() { + // Test that static tuple encoding remains the same + let args = (U256::from(100), Address::ZERO, 42u32); + + let mut buf_normal = BytesMut::new(); + let mut buf_func = BytesMut::new(); + + // Both encodings should be identical for static types + SolidityABI::encode(&args, &mut buf_normal, 0).unwrap(); + SolidityABI::encode_function_args(&args, &mut buf_func).unwrap(); + + assert_eq!(buf_normal, buf_func); + + // Decode and verify + let decoded: (U256, Address, u32) = SolidityABI::decode_function_args(&buf_func).unwrap(); + assert_eq!(decoded, args); +} + +#[test] +fn test_compact_function_args_dynamic() { + // Test CompactABI with dynamic types + let args = (vec![1u8, 2, 3], "test".to_string()); + + let mut buf_normal = BytesMut::new(); + let mut buf_func = BytesMut::new(); + + // Normal encoding (with tuple offset) + CompactABI::encode(&args, &mut buf_normal, 0).unwrap(); + + // Function args encoding (without tuple offset) + CompactABI::encode_function_args(&args, &mut buf_func).unwrap(); + + // Function args should be 4 bytes shorter (no tuple offset) + assert_eq!(buf_func.len(), buf_normal.len() - 4); + assert_eq!(&buf_func[..], &buf_normal[4..]); + + // Decode and verify + let decoded: (Vec, String) = CompactABI::decode_function_args(&buf_func).unwrap(); + assert_eq!(decoded, args); +} + +#[test] +fn test_empty_and_single_args() { + // Empty tuple + let empty = (); + let mut buf = BytesMut::new(); + + SolidityABI::encode_function_args(&empty, &mut buf).unwrap(); + assert_eq!(buf.len(), 0); + + let decoded: () = SolidityABI::decode_function_args(&buf).unwrap(); + assert_eq!(decoded, empty); + + // Single dynamic arg + let single = (Bytes::from("data"),); + buf.clear(); + + SolidityABI::encode_function_args(&single, &mut buf).unwrap(); + + // Should skip the tuple wrapper offset + let mut buf_normal = BytesMut::new(); + SolidityABI::encode(&single, &mut buf_normal, 0).unwrap(); + assert_eq!(&buf[..], &buf_normal[32..]); + + let decoded: (Bytes,) = SolidityABI::decode_function_args(&buf).unwrap(); + assert_eq!(decoded, single); +} diff --git a/crates/codec/tests/roundtrip/mod.rs b/crates/codec/tests/roundtrip/mod.rs index 2f72e4583..eebbc94fb 100644 --- a/crates/codec/tests/roundtrip/mod.rs +++ b/crates/codec/tests/roundtrip/mod.rs @@ -10,3 +10,4 @@ use fluentbase_codec::{ mod structs; mod tuples; +mod func; From 11a553876ac59b4660e6cfae40657cb7ca4968ea Mon Sep 17 00:00:00 2001 From: d1r1 Date: Fri, 12 Sep 2025 13:18:41 +0400 Subject: [PATCH 09/16] feat(sdk-derive): simplify codec with function args encoding --- crates/sdk-derive/derive-core/src/codec.rs | 160 ++------------- ..._core__client__tests__generate_client.snap | 126 +++--------- ..._core__codec__tests__complex_function.snap | 63 ++---- ...rive_core__codec__tests__empty_return.snap | 61 ++---- ..._core__codec__tests__multiple_returns.snap | 62 ++---- ..._derive_core__codec__tests__no_params.snap | 61 ++---- ..._codec__tests__simple_transfer_fluent.snap | 67 ++----- ...odec__tests__simple_transfer_solidity.snap | 63 ++---- ...e__codec__tests__single_dynamic_param.snap | 63 ++---- ..._router__tests__constructor_no_params.snap | 122 +++--------- ...uter__tests__constructor_single_param.snap | 123 +++--------- ...router__tests__constructor_two_params.snap | 187 ++++-------------- ...outer__tests__trait_router_generation.snap | 126 +++--------- 13 files changed, 235 insertions(+), 1049 deletions(-) diff --git a/crates/sdk-derive/derive-core/src/codec.rs b/crates/sdk-derive/derive-core/src/codec.rs index 70c6f38b8..561f88689 100644 --- a/crates/sdk-derive/derive-core/src/codec.rs +++ b/crates/sdk-derive/derive-core/src/codec.rs @@ -41,13 +41,8 @@ impl<'a, T: MethodLike> CodecGenerator<'a, T> { let input_types = self.extract_input_types(); let output_types = self.extract_output_types(); - let codec_impl = self.generate_codec_impl( - &call_struct, - &call_args, - &return_struct, - &return_args, - &output_types, - )?; + let codec_impl = + self.generate_codec_impl(&call_struct, &call_args, &return_struct, &return_args)?; Ok(quote! { pub type #call_args = (#(#input_types,)*); @@ -70,82 +65,21 @@ impl<'a, T: MethodLike> CodecGenerator<'a, T> { call_args: &Ident, return_struct: &Ident, return_args: &Ident, - output_types: &[&Type], ) -> Result { let crate_path = self.get_crate_path(); let codec_type = self.get_codec_type(); let selector = self.route.function_id(); let signature = self.route.parsed_signature().function_abi()?.signature()?; - let field_indices = (0..self - .route - .parsed_signature() - .inputs_without_receiver() - .len()) - .map(syn::Index::from) - .collect::>(); - - let call_encode_offset = self.create_encode_offset_expr(&codec_type, call_args); - let call_decode_offset = self.create_decode_offset_expr(&codec_type, call_args); - let return_encode_offset = self.create_encode_offset_expr(&codec_type, return_args); - let return_decode_offset = self.create_decode_offset_expr(&codec_type, return_args); - - let encode_call_impl = if field_indices.is_empty() { - quote! { - #codec_type::encode(&(), &mut buf, 0) - .expect("Failed to encode values"); - } - } else if field_indices.len() == 1 { - let index = &field_indices[0]; - quote! { - let args = self.0.clone(); - #codec_type::encode(&(args.#index,), &mut buf, 0) - .expect("Failed to encode values"); - } - } else { - quote! { - let args = self.0.clone(); - #codec_type::encode(&(#(args.#field_indices),*), &mut buf, 0) - .expect("Failed to encode values"); - } - }; - - let is_unit_type = match &self.route.parsed_signature().output { - ReturnType::Default => true, - ReturnType::Type(_, ty) => match &**ty { - Type::Tuple(tuple) => tuple.elems.is_empty(), - _ => false, - }, - }; - - let encode_return_impl = if is_unit_type { - quote! { - #codec_type::encode(&(), &mut buf, 0) - .expect("Failed to encode values"); - } - } else if output_types.len() == 1 { - quote! { - let args = self.0.clone(); - #codec_type::encode(&(args.0,), &mut buf, 0) - .expect("Failed to encode values"); - } - } else { - quote! { - let args = self.0.clone(); - #codec_type::encode(&args, &mut buf, 0) - .expect("Failed to encode values"); - } - }; - + // Encode method (with or without selector) let encode_method = if self.route.is_constructor() { quote! { /// Encodes without selector (for constructor) pub fn encode(&self) -> #crate_path::bytes::Bytes { let mut buf = #crate_path::bytes::BytesMut::new(); - #encode_call_impl - let encoded_args = buf.freeze(); - let clean_args = #call_encode_offset; - clean_args.into() + #codec_type::encode_function_args(&self.0, &mut buf) + .expect("Failed to encode values"); + buf.freeze() } } } else { @@ -153,10 +87,10 @@ impl<'a, T: MethodLike> CodecGenerator<'a, T> { /// Encodes with selector pub fn encode(&self) -> #crate_path::bytes::Bytes { let mut buf = #crate_path::bytes::BytesMut::new(); - #encode_call_impl - let encoded_args = buf.freeze(); - let clean_args = #call_encode_offset; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.extend_from_slice(&Self::SELECTOR); + #codec_type::encode_function_args(&self.0, &mut buf) + .expect("Failed to encode values"); + buf.freeze() } } }; @@ -176,13 +110,7 @@ impl<'a, T: MethodLike> CodecGenerator<'a, T> { /// Decodes call arguments from bytes pub fn decode(buf: &impl #crate_path::bytes::Buf) -> Result { - use #crate_path::bytes::BufMut; - - let mut combined_buf = #crate_path::bytes::BytesMut::new(); - combined_buf.put_slice(&#call_decode_offset); - combined_buf.put_slice(buf.chunk()); - - let args = #codec_type::<#call_args>::decode(&combined_buf.freeze(), 0)?; + let args = #codec_type::<#call_args>::decode_function_args(buf)?; Ok(Self(args)) } } @@ -203,21 +131,14 @@ impl<'a, T: MethodLike> CodecGenerator<'a, T> { /// Encodes the return values to bytes pub fn encode(&self) -> #crate_path::bytes::Bytes { let mut buf = #crate_path::bytes::BytesMut::new(); - #encode_return_impl - let encoded_args = buf.freeze(); - let clean_args = #return_encode_offset; - clean_args.into() + #codec_type::encode_function_args(&self.0, &mut buf) + .expect("Failed to encode values"); + buf.freeze() } /// Decodes return values from bytes pub fn decode(buf: &impl #crate_path::bytes::Buf) -> Result { - use #crate_path::bytes::BufMut; - - let mut combined_buf = #crate_path::bytes::BytesMut::new(); - combined_buf.put_slice(&#return_decode_offset); - combined_buf.put_slice(buf.chunk()); - - let args = #codec_type::<#return_args>::decode(&combined_buf.freeze(), 0)?; + let args = #codec_type::<#return_args>::decode_function_args(buf)?; Ok(Self(args)) } } @@ -259,55 +180,6 @@ impl<'a, T: MethodLike> CodecGenerator<'a, T> { } } - /// Creates the encode offset expression based on the mode - /// Important: Dynamic types need the first 32/4 bytes removed - fn create_encode_offset_expr( - &self, - codec_type: &TokenStream2, - type_name: &Ident, - ) -> TokenStream2 { - match self.mode { - Mode::Solidity => quote! { - if #codec_type::<#type_name>::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - } - }, - Mode::Fluent => quote! { - if #codec_type::<#type_name>::is_dynamic() { - encoded_args[4..].to_vec() - } else { - encoded_args.to_vec() - } - }, - } - } - - /// Creates the decode offset expression based on the mode - fn create_decode_offset_expr( - &self, - codec_type: &TokenStream2, - type_name: &Ident, - ) -> TokenStream2 { - match self.mode { - Mode::Solidity => quote! { - if #codec_type::<#type_name>::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - } - }, - Mode::Fluent => quote! { - if #codec_type::<#type_name>::is_dynamic() { - (4_u32).to_le_bytes().to_vec() - } else { - ::alloc::vec::Vec::new() - } - }, - } - } - /// Determines the appropriate crate path to use for codec implementations fn get_crate_path(&self) -> TokenStream2 { match std::env::var("CARGO_PKG_NAME").as_deref() { @@ -324,7 +196,7 @@ impl<'a, T: MethodLike> CodecGenerator<'a, T> { let crate_path = self.get_crate_path(); match self.mode { Mode::Solidity => quote! { #crate_path::encoder::SolidityABI }, - Mode::Fluent => quote! { #crate_path::encoder::FluentABI }, + Mode::Fluent => quote! { #crate_path::encoder::CompactABI }, } } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__client__tests__generate_client.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__client__tests__generate_client.snap index 2b61bd032..4818362df 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__client__tests__generate_client.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__client__tests__generate_client.snap @@ -23,39 +23,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - FirstMethodCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - FirstMethodCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< FirstMethodCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -73,39 +55,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - FirstMethodReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - FirstMethodReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< FirstMethodReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -133,43 +96,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode( - &(args.0, args.1), + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, &mut buf, - 0, ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - SecondMethodCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - SecondMethodCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< SecondMethodCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -187,39 +128,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&args, &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - SecondMethodReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - SecondMethodReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< SecondMethodReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__complex_function.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__complex_function.snap index 0002900cb..28b6155e4 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__complex_function.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__complex_function.snap @@ -19,43 +19,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode( - &(args.0, args.1), + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, &mut buf, - 0, ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - ComplexOpCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - ComplexOpCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< ComplexOpCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -73,39 +51,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&args, &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - ComplexOpReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - ComplexOpReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< ComplexOpReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__empty_return.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__empty_return.snap index ebe4a4843..872286bf4 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__empty_return.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__empty_return.snap @@ -19,38 +19,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - EmptyReturnCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - EmptyReturnCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< EmptyReturnCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -68,38 +51,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - EmptyReturnReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - EmptyReturnReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< EmptyReturnReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__multiple_returns.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__multiple_returns.snap index 9b28a0660..b1472daaf 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__multiple_returns.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__multiple_returns.snap @@ -19,38 +19,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - MultiReturnCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - MultiReturnCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< MultiReturnCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -68,39 +51,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&args, &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - MultiReturnReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - MultiReturnReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< MultiReturnReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__no_params.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__no_params.snap index 2acabe339..4122a562e 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__no_params.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__no_params.snap @@ -19,38 +19,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - NoParamsCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - NoParamsCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< NoParamsCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -68,38 +51,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - NoParamsReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - NoParamsReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< NoParamsReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_fluent.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_fluent.snap index 618055912..dc3e4c19a 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_fluent.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_fluent.snap @@ -19,43 +19,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::FluentABI::encode( - &(args.0, args.1), + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::CompactABI::encode_function_args( + &self.0, &mut buf, - 0, ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::FluentABI::< - TransferCallArgs, - >::is_dynamic() { - encoded_args[4..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::FluentABI::< - TransferCallArgs, - >::is_dynamic() { - (4_u32).to_le_bytes().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); - let args = fluentbase_sdk::codec::encoder::FluentABI::< + let args = fluentbase_sdk::codec::encoder::CompactABI::< TransferCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -73,39 +51,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::FluentABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::CompactABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::FluentABI::< - TransferReturnArgs, - >::is_dynamic() { - encoded_args[4..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::FluentABI::< - TransferReturnArgs, - >::is_dynamic() { - (4_u32).to_le_bytes().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); - let args = fluentbase_sdk::codec::encoder::FluentABI::< + let args = fluentbase_sdk::codec::encoder::CompactABI::< TransferReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_solidity.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_solidity.snap index a4f2e30ba..af1ed294c 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_solidity.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_solidity.snap @@ -19,43 +19,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode( - &(args.0, args.1), + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, &mut buf, - 0, ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - TransferCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - TransferCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< TransferCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -73,39 +51,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - TransferReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - TransferReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< TransferReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__single_dynamic_param.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__single_dynamic_param.snap index bb144cb32..21a7370f5 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__single_dynamic_param.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__single_dynamic_param.snap @@ -19,39 +19,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - CustomGreetingCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - CustomGreetingCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< CustomGreetingCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -69,39 +51,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - CustomGreetingReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - CustomGreetingReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< CustomGreetingReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_no_params.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_no_params.snap index d0158b274..fd30f00f2 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_no_params.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_no_params.snap @@ -27,38 +27,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - GetValueCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - GetValueCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< GetValueCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -76,39 +59,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - GetValueReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - GetValueReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< GetValueReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -136,38 +100,20 @@ const _: () = { /// Encodes without selector (for constructor) pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< ConstructorCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -185,38 +131,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< ConstructorReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_single_param.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_single_param.snap index d3a46b334..4b835e46d 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_single_param.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_single_param.snap @@ -27,38 +27,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - GetValueCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - GetValueCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< GetValueCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -76,39 +59,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - GetValueReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - GetValueReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< GetValueReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -136,39 +100,20 @@ const _: () = { /// Encodes without selector (for constructor) pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< ConstructorCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -186,38 +131,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< ConstructorReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_two_params.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_two_params.snap index 5089f7fa9..2f4757c7e 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_two_params.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_two_params.snap @@ -30,39 +30,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - BalanceOfCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - BalanceOfCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< BalanceOfCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -80,39 +62,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - BalanceOfReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - BalanceOfReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< BalanceOfReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -140,43 +103,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode( - &(args.0, args.1), + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, &mut buf, - 0, ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - TransferCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - TransferCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< TransferCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -194,39 +135,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - TransferReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - TransferReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< TransferReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -254,43 +176,20 @@ const _: () = { /// Encodes without selector (for constructor) pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode( - &(args.0, args.1), + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, &mut buf, - 0, ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< ConstructorCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -308,38 +207,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - ConstructorReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< ConstructorReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__trait_router_generation.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__trait_router_generation.snap index bfe28ba59..692675b48 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__trait_router_generation.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__trait_router_generation.snap @@ -30,43 +30,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode( - &(args.0, args.1, args.2), + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, &mut buf, - 0, ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - GreetingCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - GreetingCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< GreetingCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -84,39 +62,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - GreetingReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - GreetingReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< GreetingReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -144,39 +103,21 @@ const _: () = { /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + buf.extend_from_slice(&Self::SELECTOR); + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - CustomGreetingCallArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - Self::SELECTOR.iter().copied().chain(clean_args).collect() + buf.freeze() } /// Decodes call arguments from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - CustomGreetingCallArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< CustomGreetingCallArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } @@ -194,39 +135,20 @@ const _: () = { /// Encodes the return values to bytes pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - let args = self.0.clone(); - fluentbase_sdk::codec::encoder::SolidityABI::encode(&(args.0,), &mut buf, 0) + fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( + &self.0, + &mut buf, + ) .expect("Failed to encode values"); - let encoded_args = buf.freeze(); - let clean_args = if fluentbase_sdk::codec::encoder::SolidityABI::< - CustomGreetingReturnArgs, - >::is_dynamic() { - encoded_args[32..].to_vec() - } else { - encoded_args.to_vec() - }; - clean_args.into() + buf.freeze() } /// Decodes return values from bytes pub fn decode( buf: &impl fluentbase_sdk::codec::bytes::Buf, ) -> Result { - use fluentbase_sdk::codec::bytes::BufMut; - let mut combined_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - combined_buf - .put_slice( - &if fluentbase_sdk::codec::encoder::SolidityABI::< - CustomGreetingReturnArgs, - >::is_dynamic() { - ::fluentbase_sdk::U256::from(32).to_be_bytes::<32>().to_vec() - } else { - ::alloc::vec::Vec::new() - }, - ); - combined_buf.put_slice(buf.chunk()); let args = fluentbase_sdk::codec::encoder::SolidityABI::< CustomGreetingReturnArgs, - >::decode(&combined_buf.freeze(), 0)?; + >::decode_function_args(buf)?; Ok(Self(args)) } } From 2b23ed3d8a17e5095916920ffae7fb90895c9289 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Fri, 12 Sep 2025 15:31:18 +0400 Subject: [PATCH 10/16] fix(sdk-derive): separate selector from args in the codec encode method --- crates/sdk-derive/derive-core/src/codec.rs | 9 +++++++-- ...ive_core__client__tests__generate_client.snap | 16 ++++++++++------ ...ive_core__codec__tests__complex_function.snap | 8 +++++--- ..._derive_core__codec__tests__empty_return.snap | 8 +++++--- ...ive_core__codec__tests__multiple_returns.snap | 8 +++++--- ...sdk_derive_core__codec__tests__no_params.snap | 8 +++++--- ...re__codec__tests__simple_transfer_fluent.snap | 8 +++++--- ...__codec__tests__simple_transfer_solidity.snap | 8 +++++--- ...core__codec__tests__single_dynamic_param.snap | 8 +++++--- ...re__router__tests__constructor_no_params.snap | 8 +++++--- ..._router__tests__constructor_single_param.snap | 8 +++++--- ...e__router__tests__constructor_two_params.snap | 16 ++++++++++------ ...__router__tests__trait_router_generation.snap | 16 ++++++++++------ 13 files changed, 82 insertions(+), 47 deletions(-) diff --git a/crates/sdk-derive/derive-core/src/codec.rs b/crates/sdk-derive/derive-core/src/codec.rs index 561f88689..44464fc03 100644 --- a/crates/sdk-derive/derive-core/src/codec.rs +++ b/crates/sdk-derive/derive-core/src/codec.rs @@ -86,10 +86,15 @@ impl<'a, T: MethodLike> CodecGenerator<'a, T> { quote! { /// Encodes with selector pub fn encode(&self) -> #crate_path::bytes::Bytes { + // First encode the arguments separately + let mut args_buf = #crate_path::bytes::BytesMut::new(); + #codec_type::encode_function_args(&self.0, &mut args_buf) + .expect("Failed to encode values"); + + // Then combine selector + encoded args let mut buf = #crate_path::bytes::BytesMut::new(); buf.extend_from_slice(&Self::SELECTOR); - #codec_type::encode_function_args(&self.0, &mut buf) - .expect("Failed to encode values"); + buf.extend_from_slice(&args_buf); buf.freeze() } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__client__tests__generate_client.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__client__tests__generate_client.snap index 4818362df..042037f3d 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__client__tests__generate_client.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__client__tests__generate_client.snap @@ -22,13 +22,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes @@ -95,13 +97,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__complex_function.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__complex_function.snap index 28b6155e4..df26cd9ed 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__complex_function.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__complex_function.snap @@ -18,13 +18,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__empty_return.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__empty_return.snap index 872286bf4..444cd7242 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__empty_return.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__empty_return.snap @@ -18,13 +18,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__multiple_returns.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__multiple_returns.snap index b1472daaf..351f8aef1 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__multiple_returns.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__multiple_returns.snap @@ -18,13 +18,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__no_params.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__no_params.snap index 4122a562e..3029dff9e 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__no_params.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__no_params.snap @@ -18,13 +18,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_fluent.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_fluent.snap index dc3e4c19a..7c26ec504 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_fluent.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_fluent.snap @@ -18,13 +18,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::CompactABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_solidity.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_solidity.snap index af1ed294c..9fb704cc8 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_solidity.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__simple_transfer_solidity.snap @@ -18,13 +18,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__single_dynamic_param.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__single_dynamic_param.snap index 21a7370f5..7ed57bd94 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__single_dynamic_param.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__codec__tests__single_dynamic_param.snap @@ -18,13 +18,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_no_params.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_no_params.snap index fd30f00f2..4e0fdf363 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_no_params.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_no_params.snap @@ -26,13 +26,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_single_param.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_single_param.snap index 4b835e46d..962bc20a6 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_single_param.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_single_param.snap @@ -26,13 +26,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_two_params.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_two_params.snap index 2f4757c7e..61b2b9cf8 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_two_params.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__constructor_two_params.snap @@ -29,13 +29,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes @@ -102,13 +104,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__trait_router_generation.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__trait_router_generation.snap index 692675b48..2bb719d03 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__trait_router_generation.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__router__tests__trait_router_generation.snap @@ -29,13 +29,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes @@ -102,13 +104,15 @@ const _: () = { } /// Encodes with selector pub fn encode(&self) -> fluentbase_sdk::codec::bytes::Bytes { - let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); - buf.extend_from_slice(&Self::SELECTOR); + let mut args_buf = fluentbase_sdk::codec::bytes::BytesMut::new(); fluentbase_sdk::codec::encoder::SolidityABI::encode_function_args( &self.0, - &mut buf, + &mut args_buf, ) .expect("Failed to encode values"); + let mut buf = fluentbase_sdk::codec::bytes::BytesMut::new(); + buf.extend_from_slice(&Self::SELECTOR); + buf.extend_from_slice(&args_buf); buf.freeze() } /// Decodes call arguments from bytes From e9153ac800de8b333555736cb75ea9c2927b8f32 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Fri, 12 Sep 2025 15:32:10 +0400 Subject: [PATCH 11/16] refactor(examples): migrate erc20 to new storage system --- crates/sdk/src/entrypoint.rs | 27 ++ e2e/src/deployer.rs | 14 +- e2e/src/evm.rs | 28 +- e2e/src/wasm.rs | 18 +- examples/erc20/lib.rs | 692 ++++++++++++++++++++++------------- 5 files changed, 518 insertions(+), 261 deletions(-) diff --git a/crates/sdk/src/entrypoint.rs b/crates/sdk/src/entrypoint.rs index 60f507da6..66a95c15e 100644 --- a/crates/sdk/src/entrypoint.rs +++ b/crates/sdk/src/entrypoint.rs @@ -25,6 +25,33 @@ macro_rules! basic_entrypoint { pub fn main() {} }; } +#[macro_export] +macro_rules! entrypoint_with_storage { + ($struct_typ:ident) => { + #[cfg(target_arch = "wasm32")] + #[no_mangle] + extern "C" fn deploy() { + use fluentbase_sdk::{rwasm::RwasmContext, shared::SharedContextImpl, U256}; + let mut sdk = SharedContextImpl::new(RwasmContext {}); + let mut app = $struct_typ::new(sdk, U256::from(0), 0); + app.deploy(); + } + #[cfg(target_arch = "wasm32")] + #[no_mangle] + extern "C" fn main() { + use fluentbase_sdk::{rwasm::RwasmContext, shared::SharedContextImpl}; + let mut sdk = SharedContextImpl::new(RwasmContext {}); + let mut app = $struct_typ::new(sdk, U256::from(0), 0); + app.main(); + } + #[cfg(target_arch = "wasm32")] + $crate::define_panic_handler!(); + #[cfg(target_arch = "wasm32")] + $crate::define_allocator!(); + #[cfg(not(target_arch = "wasm32"))] + pub fn main() {} + }; +} #[macro_export] macro_rules! func_entrypoint { diff --git a/e2e/src/deployer.rs b/e2e/src/deployer.rs index 1e8895016..e528e060a 100644 --- a/e2e/src/deployer.rs +++ b/e2e/src/deployer.rs @@ -1,6 +1,7 @@ use crate::EvmTestingContextWithGenesis; use alloy_sol_types::{sol, SolCall, SolValue}; -use fluentbase_sdk::{Address, Bytes}; +use fluentbase_sdk::{hex, Address, Bytes}; +use fluentbase_sdk::constructor::encode_constructor_params; use fluentbase_sdk_testing::EvmTestingContext; /// Contract `ContractDeployer.sol` is a smart contract that deploys @@ -65,5 +66,14 @@ fn test_evm_create_wasm_contract() { #[test] fn test_evm_create_large_wasm_contract() { let mut ctx = EvmTestingContext::default().with_full_genesis(); - deploy_via_deployer(&mut ctx, crate::EXAMPLE_ERC20.into()); + + // Add constructor parameters for ERC20 + let bytecode: &[u8] = crate::EXAMPLE_ERC20.into(); + let constructor_params = hex!("000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000000954657374546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035453540000000000000000000000000000000000000000000000000000000000"); + let encoded_constructor_params = encode_constructor_params(&constructor_params); + let mut input: Vec = Vec::new(); + input.extend(bytecode); + input.extend(encoded_constructor_params); + + deploy_via_deployer(&mut ctx, input.into()); } diff --git a/e2e/src/evm.rs b/e2e/src/evm.rs index 2b04dd62b..52770bab0 100644 --- a/e2e/src/evm.rs +++ b/e2e/src/evm.rs @@ -1,15 +1,21 @@ use crate::EvmTestingContextWithGenesis; use alloy_sol_types::{sol, SolCall}; use core::str::from_utf8; -use fluentbase_sdk::{address, bytes, calc_create_address, Address, U256}; +use fluentbase_sdk::{address, bytes, calc_create_address, constructor, Address, U256}; use fluentbase_sdk_testing::{ - try_print_utf8_error, EvmTestingContext, HostTestingContextNativeAPI, TxBuilder, + try_print_utf8_error, + EvmTestingContext, + HostTestingContextNativeAPI, + TxBuilder, }; use fluentbase_types::{PRECOMPILE_BLAKE2F, PRECOMPILE_SECP256K1_RECOVER}; use hex_literal::hex; use revm::{ - bytecode::opcode, context::result::ExecutionResult::Revert, primitives::hardfork::SpecId, + bytecode::opcode, + context::result::ExecutionResult::Revert, + primitives::hardfork::SpecId, }; +use fluentbase_sdk::constructor::encode_constructor_params; #[test] fn test_evm_greeting() { @@ -307,7 +313,21 @@ fn test_evm_balance() { fn test_wasm_erc20() { let mut ctx = EvmTestingContext::default().with_full_genesis(); const OWNER_ADDRESS: Address = Address::ZERO; - let contract_address = ctx.deploy_evm_tx(OWNER_ADDRESS, crate::EXAMPLE_ERC20.into()); + let bytecode: &[u8] = crate::EXAMPLE_ERC20.into(); + + // constructor params for ERC20: + // name: "TestToken" + // symbol: "TST" + // initial_supply: 1_000_000 + // use examples/erc20/src/lib.rs print_constructor_params_hex() to regenerate + let constructor_params = hex!("000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000000954657374546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035453540000000000000000000000000000000000000000000000000000000000"); + + let encoded_constructor_params = encode_constructor_params(&constructor_params); + let mut input: Vec = Vec::new(); + input.extend(bytecode); + input.extend(encoded_constructor_params); + + let contract_address = ctx.deploy_evm_tx(OWNER_ADDRESS, input.into()); let transfer_coin = |ctx: &mut EvmTestingContext| { let result = ctx.call_evm_tx( OWNER_ADDRESS, diff --git a/e2e/src/wasm.rs b/e2e/src/wasm.rs index dc2ed5558..0699bbdf0 100644 --- a/e2e/src/wasm.rs +++ b/e2e/src/wasm.rs @@ -19,6 +19,7 @@ use hex_literal::hex; use revm::bytecode::Bytecode; use rwasm::RwasmModule; use std::str::from_utf8_unchecked; +use fluentbase_sdk::constructor::encode_constructor_params; #[test] fn test_wasm_greeting() { @@ -147,7 +148,22 @@ fn test_wasm_panic() { fn test_wasm_erc20() { let mut ctx = EvmTestingContext::default().with_minimal_genesis(); const OWNER_ADDRESS: Address = Address::ZERO; - let contract_address = ctx.deploy_evm_tx(OWNER_ADDRESS, EXAMPLE_ERC20.into()); + + // Add constructor parameters + let bytecode: &[u8] = EXAMPLE_ERC20.into(); + // constructor params for ERC20: + // name: "TestToken" + // symbol: "TST" + // initial_supply: 1_000_000 + // use examples/erc20/src/lib.rs print_constructor_params_hex() to regenerate + let constructor_params = hex!("000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000f4240000000000000000000000000000000000000000000000000000000000000000954657374546f6b656e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000035453540000000000000000000000000000000000000000000000000000000000"); + let encoded_constructor_params = encode_constructor_params(&constructor_params); + let mut input: Vec = Vec::new(); + input.extend(bytecode); + input.extend(encoded_constructor_params); + + let contract_address = ctx.deploy_evm_tx(OWNER_ADDRESS, input.into()); + // call with empty input (should fail) let result = ctx.call_evm_tx( OWNER_ADDRESS, diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index 44279d435..952fbcd76 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -1,33 +1,30 @@ -#![cfg_attr(target_arch = "wasm32", no_std, no_main)] +#![cfg_attr(not(feature = "std"), no_std, no_main)] #![allow(dead_code)] + extern crate alloc; extern crate fluentbase_sdk; -use alloc::vec::Vec; +use alloc::{string::String, vec::Vec}; use alloy_sol_types::{sol, SolEvent}; use fluentbase_sdk::{ - basic_entrypoint, - derive::{router, solidity_storage}, + derive::{router, Storage}, + entrypoint_with_storage, + storage::{ + bytes::StorageString, + composite::Composite, + map::StorageMap, + primitive::StoragePrimitive, + BytesAccess, + MapAccess, + PrimitiveAccess, + }, Address, - Bytes, ContextReader, SharedAPI, B256, U256, }; -pub trait ERC20API { - fn symbol(&self) -> Bytes; - fn name(&self) -> Bytes; - fn decimals(&self) -> U256; - fn total_supply(&self) -> U256; - fn balance_of(&self, address: Address) -> U256; - fn transfer(&mut self, to: Address, value: U256) -> U256; - fn allowance(&self, owner: Address, spender: Address) -> U256; - fn approve(&mut self, spender: Address, value: U256) -> U256; - fn transfer_from(&mut self, from: Address, to: Address, value: U256) -> U256; -} - // Define the Transfer and Approval events sol! { event Transfer(address indexed from, address indexed to, uint256 value); @@ -44,114 +41,114 @@ fn emit_event(sdk: &mut SDK, event: T) { sdk.emit_log(&topics, &data); } -solidity_storage! { - mapping(Address => U256) Balance; - mapping(Address => mapping(Address => U256)) Allowance; -} - -impl Balance { - fn add( - sdk: &mut SDK, - address: Address, - amount: U256, - ) -> Result<(), &'static str> { - let current_balance = Self::get(sdk, address); - let new_balance = current_balance + amount; - Self::set(sdk, address, new_balance); - Ok(()) - } - fn subtract( - sdk: &mut SDK, - address: Address, - amount: U256, - ) -> Result<(), &'static str> { - let current_balance = Self::get(sdk, address); - if current_balance < amount { - return Err("insufficient balance"); - } - let new_balance = current_balance - amount; - Self::set(sdk, address, new_balance); - Ok(()) - } +// Storage structures +#[derive(Storage)] +pub struct ERC20Storage { + pub token_name: StorageString, + pub token_symbol: StorageString, + pub total_supply: StoragePrimitive, + pub balances: StorageMap>, + pub allowances: StorageMap>>, } -impl Allowance { - fn add( - sdk: &mut SDK, - owner: Address, - spender: Address, - amount: U256, - ) -> Result<(), &'static str> { - let current_allowance = Self::get(sdk, owner, spender); - let new_allowance = current_allowance + amount; - Self::set(sdk, owner, spender, new_allowance); - Ok(()) - } - fn subtract( - sdk: &mut SDK, - owner: Address, - spender: Address, - amount: U256, - ) -> Result<(), &'static str> { - let current_allowance = Self::get(sdk, owner, spender); - if current_allowance < amount { - return Err("insufficient allowance"); - } - let new_allowance = current_allowance - amount; - Self::set(sdk, owner, spender, new_allowance); - Ok(()) - } -} - -struct ERC20 { +#[derive(Storage)] +pub struct ERC20 { sdk: SDK, + storage: Composite, } +#[router(mode = "solidity")] impl ERC20 { - pub fn new(sdk: SDK) -> Self { - ERC20 { sdk } + pub fn constructor(&mut self, name: String, symbol: String, initial_supply: U256) { + // Set token metadata + self.storage().token_name().set_string(&mut self.sdk, &name); + self.storage() + .token_symbol() + .set_string(&mut self.sdk, &symbol); + self.storage() + .total_supply() + .set(&mut self.sdk, initial_supply); + + // Assign initial supply to deployer + let deployer = self.sdk.context().contract_caller(); + self.storage() + .balances() + .entry(deployer) + .set(&mut self.sdk, initial_supply); + + // Emit initial transfer event from zero address + emit_event( + &mut self.sdk, + Transfer { + from: Address::ZERO, + to: deployer, + value: initial_supply, + }, + ); } -} -#[router(mode = "solidity")] -impl ERC20API for ERC20 { - fn symbol(&self) -> Bytes { - Bytes::from("TOK") + pub fn name(&self) -> String { + self.storage().token_name().get_string(&self.sdk) } - fn name(&self) -> Bytes { - Bytes::from("Token") + pub fn symbol(&self) -> String { + self.storage().token_symbol().get_string(&self.sdk) } - fn decimals(&self) -> U256 { + pub fn decimals(&self) -> U256 { U256::from(18) } - fn total_supply(&self) -> U256 { - U256::from_str_radix("1000000000000000000000000", 10).unwrap() + pub fn total_supply(&self) -> U256 { + self.storage().total_supply().get(&self.sdk) } - fn balance_of(&self, address: Address) -> U256 { - Balance::get(&self.sdk, address) + pub fn balance_of(&self, account: Address) -> U256 { + self.storage().balances().entry(account).get(&self.sdk) } - fn transfer(&mut self, to: Address, value: U256) -> U256 { + pub fn transfer(&mut self, to: Address, value: U256) -> U256 { let from = self.sdk.context().contract_caller(); - Balance::subtract(&mut self.sdk, from, value).unwrap_or_else(|err| panic!("{}", err)); - Balance::add(&mut self.sdk, to, value).unwrap_or_else(|err| panic!("{}", err)); + // Check sufficient balance + let from_balance = self.storage().balances().entry(from).get(&self.sdk); + if from_balance < value { + panic!("insufficient balance"); + } + + // Update balances + self.storage() + .balances() + .entry(from) + .set(&mut self.sdk, from_balance - value); + + let to_balance = self.storage().balances().entry(to).get(&self.sdk); + self.storage() + .balances() + .entry(to) + .set(&mut self.sdk, to_balance + value); emit_event(&mut self.sdk, Transfer { from, to, value }); U256::from(1) } - fn allowance(&self, owner: Address, spender: Address) -> U256 { - Allowance::get(&self.sdk, owner, spender) + pub fn allowance(&self, owner: Address, spender: Address) -> U256 { + self.storage() + .allowances() + .entry(owner) + .entry(spender) + .get(&self.sdk) } - fn approve(&mut self, spender: Address, value: U256) -> U256 { + pub fn approve(&mut self, spender: Address, value: U256) -> U256 { let owner = self.sdk.context().contract_caller(); - Allowance::set(&mut self.sdk, owner, spender, value); + + self.storage() + .allowances() + .entry(owner) + .entry(spender) + .set(&mut self.sdk, value); + emit_event( &mut self.sdk, Approval { @@ -163,220 +160,407 @@ impl ERC20API for ERC20 { U256::from(1) } - fn transfer_from(&mut self, from: Address, to: Address, value: U256) -> U256 { + pub fn transfer_from(&mut self, from: Address, to: Address, value: U256) -> U256 { let spender = self.sdk.context().contract_caller(); - let current_allowance = Allowance::get(&self.sdk, from, spender); + // Check allowance + let current_allowance = self + .storage() + .allowances() + .entry(from) + .entry(spender) + .get(&self.sdk); + if current_allowance < value { panic!("insufficient allowance"); } - Allowance::subtract(&mut self.sdk, from, spender, value) - .unwrap_or_else(|err| panic!("{}", err)); - Balance::subtract(&mut self.sdk, from, value).unwrap_or_else(|err| panic!("{}", err)); - Balance::add(&mut self.sdk, to, value).unwrap_or_else(|err| panic!("{}", err)); + // Check balance + let from_balance = self.storage().balances().entry(from).get(&self.sdk); + if from_balance < value { + panic!("insufficient balance"); + } + + // Update allowance + self.storage() + .allowances() + .entry(from) + .entry(spender) + .set(&mut self.sdk, current_allowance - value); + + // Update balances + self.storage() + .balances() + .entry(from) + .set(&mut self.sdk, from_balance - value); + + let to_balance = self.storage().balances().entry(to).get(&self.sdk); + self.storage() + .balances() + .entry(to) + .set(&mut self.sdk, to_balance + value); emit_event(&mut self.sdk, Transfer { from, to, value }); U256::from(1) } } -impl ERC20 { - pub fn deploy(&mut self) { - let owner_address = self.sdk.context().contract_caller(); - let owner_balance: U256 = U256::from_str_radix("1000000000000000000000000", 10).unwrap(); - - let _ = Balance::add(&mut self.sdk, owner_address, owner_balance); - } -} - -basic_entrypoint!(ERC20); +entrypoint_with_storage!(ERC20); #[cfg(test)] -mod test { +mod tests { use super::*; - use fluentbase_sdk::{address, ContractContextV1}; + use fluentbase_sdk::{ + address, + byteorder, + bytes::Buf, + codec::Encoder, + storage::{BytesAccess, MapAccess, PrimitiveAccess}, + Address, + ContractContextV1, + U256, + }; use fluentbase_sdk_testing::HostTestingContext; - use hex_literal::hex; - use serial_test::serial; - #[serial] #[test] - pub fn test_deploy() { - let owner_address = Address::from(hex!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")); - let owner_balance = U256::from_str_radix("1000000000000000000000000", 10).unwrap(); - // set up the test input with the owner's address as the contract caller - let sdk = HostTestingContext::default().with_contract_context(ContractContextV1 { - caller: owner_address, - ..Default::default() - }); - let mut erc20 = ERC20::new(sdk); - // call the deployment function to initialize the contract state - erc20.deploy(); - // verify the balance - let balance = Balance::get(&mut erc20.sdk, owner_address); - assert_eq!(balance, owner_balance); - } + fn test_constructor_initializes_correctly() { + // Arrange + let deployer = address!("1111111111111111111111111111111111111111"); + let token_name = "MyToken".to_string(); + let token_symbol = "MTK".to_string(); + let initial_supply = U256::from(1_000_000); - #[serial] - #[test] - pub fn test_name() { - let call_name = NameCall::new(()).encode(); - let sdk = HostTestingContext::default().with_input(call_name); - let mut erc20 = ERC20::new(sdk.clone()); - erc20.deploy(); - erc20.main(); - let result = sdk.take_output(); - let val = SymbolReturn::decode(&result.as_slice()).unwrap(); - let symbol = String::from_utf8_lossy(&val.0 .0); - assert_eq!(symbol, "Token"); - } + let constructor_call = + ConstructorCall::new((token_name.clone(), token_symbol.clone(), initial_supply)); - #[serial] - #[test] - pub fn test_symbol() { - let call_symbol = SymbolCall::new(()).encode(); - let sdk = HostTestingContext::default().with_input(call_symbol); - let mut erc20 = ERC20::new(sdk.clone()); - erc20.deploy(); - erc20.main(); - let result = sdk.take_output(); - let val = SymbolReturn::decode(&result.as_slice()).unwrap(); - let symbol = String::from_utf8_lossy(&val.0 .0); - assert_eq!(symbol, "TOK"); - } + let sdk = HostTestingContext::default() + .with_input(constructor_call.encode()) + .with_contract_context(ContractContextV1 { + address: address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + caller: deployer, + ..Default::default() + }); - #[serial] - #[test] - pub fn test_balance_of() { - let owner_address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); - let expected_balance = "1000000000000000000000000"; - let sdk = HostTestingContext::default().with_contract_context(ContractContextV1 { - caller: owner_address, - ..Default::default() - }); - let mut erc20 = ERC20::new(sdk.clone()); - erc20.deploy(); + // Act + let mut contract = ERC20::new(sdk.clone(), U256::from(0), 0); + contract.deploy(); + + // Assert - verify storage was initialized correctly assert_eq!( - Balance::get(&mut erc20.sdk, owner_address).to_string(), - expected_balance + contract.storage().token_name().get_string(&sdk), + token_name, + "Token name not set correctly" + ); + + assert_eq!( + contract.storage().token_symbol().get_string(&sdk), + token_symbol, + "Token symbol not set correctly" + ); + + assert_eq!( + contract.storage().total_supply().get(&sdk), + initial_supply, + "Total supply not set correctly" + ); + + // Verify deployer received initial supply + assert_eq!( + contract.storage().balances().entry(deployer).get(&sdk), + initial_supply, + "Deployer did not receive initial supply" + ); + + // Verify other addresses have zero balance + let other_address = address!("2222222222222222222222222222222222222222"); + assert_eq!( + contract.storage().balances().entry(other_address).get(&sdk), + U256::ZERO, + "Non-deployer address should have zero balance" ); - let call_balance_of = - hex!("70a08231000000000000000000000000f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); - let sdk = sdk.with_input(call_balance_of); - erc20.main(); - let result = sdk.take_output(); - let output_balance = U256::from_be_slice(&result); - assert_eq!(output_balance.to_string(), expected_balance); } - #[serial] #[test] - pub fn test_transfer() { - let from = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); - let to = address!("390a4CEdBb65be7511D9E1a35b115376F39DbDF3"); - let value = U256::from_str_radix("100000000000000000000", 10).unwrap(); - let sdk = HostTestingContext::default().with_contract_context(ContractContextV1 { - caller: from, - ..Default::default() - }); - let mut erc20 = ERC20::new(sdk.clone()); - // run constructor - erc20.deploy(); - // check balances - // let balance_from = erc20.balances.get(from); + fn test_basic_query_functions() { + // Arrange + let deployer = address!("1111111111111111111111111111111111111111"); + let token_name = "TestToken".to_string(); + let token_symbol = "TST".to_string(); + let initial_supply = U256::from(10_000_000); + + // Initialize contract with constructor + let constructor_call = + ConstructorCall::new((token_name.clone(), token_symbol.clone(), initial_supply)); + + let sdk = HostTestingContext::default() + .with_input(constructor_call.encode()) + .with_contract_context(ContractContextV1 { + address: address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + caller: deployer, + ..Default::default() + }); + + let mut contract = ERC20::new(sdk.clone(), U256::from(0), 0); + contract.deploy(); + + // Act & Assert - Test name() getter + contract.sdk = contract.sdk.with_input(NameCall::new(()).encode()); + contract.main(); + let name_result = NameReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); assert_eq!( - Balance::get(&mut erc20.sdk, from).to_string(), - "1000000000000000000000000" + name_result.0 .0, token_name, + "name() returned incorrect value" ); - assert_eq!(Balance::get(&mut erc20.sdk, to).to_string(), "0"); - // transfer funds (100 tokens) - let _sdk = sdk.with_input(TransferCall((to, value)).encode()); - erc20.main(); - // check balances again + + // Test symbol() getter + contract.sdk = contract.sdk.with_input(SymbolCall::new(()).encode()); + contract.main(); + let symbol_result = SymbolReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); assert_eq!( - Balance::get(&mut erc20.sdk, from).to_string(), - "999900000000000000000000" + symbol_result.0 .0, token_symbol, + "symbol() returned incorrect value" ); + + // Test decimals() getter + contract.sdk = contract.sdk.with_input(DecimalsCall::new(()).encode()); + contract.main(); + let decimals_result = DecimalsReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); assert_eq!( - Balance::get(&mut erc20.sdk, to).to_string(), - "100000000000000000000" + decimals_result.0 .0, + U256::from(18), + "decimals() should return 18" + ); + + // Test total_supply() getter + contract.sdk = contract.sdk.with_input(TotalSupplyCall::new(()).encode()); + contract.main(); + let total_supply_result = + TotalSupplyReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); + assert_eq!( + total_supply_result.0 .0, initial_supply, + "total_supply() returned incorrect value" + ); + + // ----> current + // Test balance_of() for deployer + contract.sdk = contract + .sdk + .with_input(BalanceOfCall::new((deployer,)).encode()); + contract.main(); + let balance_result = BalanceOfReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); + assert_eq!( + balance_result.0 .0, initial_supply, + "balance_of(deployer) should equal initial supply" + ); + + // Test balance_of() for zero address + let zero_address = Address::ZERO; + contract.sdk = contract + .sdk + .with_input(BalanceOfCall::new((zero_address,)).encode()); + contract.main(); + let zero_balance_result = + BalanceOfReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); + assert_eq!( + zero_balance_result.0 .0, + U256::ZERO, + "balance_of(0x0) should return 0" + ); + + // Test balance_of() for random address + let random_address = address!("9999999999999999999999999999999999999999"); + contract.sdk = contract + .sdk + .with_input(BalanceOfCall::new((random_address,)).encode()); + contract.main(); + let random_balance_result = + BalanceOfReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); + assert_eq!( + random_balance_result.0 .0, + U256::ZERO, + "balance_of(random_address) should return 0" ); } - #[serial] #[test] - pub fn test_allowance() { - let owner = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); - let spender = address!("390a4CEdBb65be7511D9E1a35b115376F39DbDF3"); - let approve_call = ApproveCall::new((spender, U256::from(1000))).encode(); + fn test_balance_of_bug() { + // Arrange + let deployer = address!("1111111111111111111111111111111111111111"); + let token_name = "TestToken".to_string(); + let token_symbol = "TST".to_string(); + let initial_supply = U256::from(10_000_000); + + // Initialize contract with constructor + let constructor_call = + ConstructorCall::new((token_name.clone(), token_symbol.clone(), initial_supply)); let sdk = HostTestingContext::default() + .with_input(constructor_call.encode()) .with_contract_context(ContractContextV1 { - caller: owner, + address: address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + caller: deployer, ..Default::default() - }) - .with_input(approve_call); - let mut erc20 = ERC20::new(sdk.clone()); - - // approve allowance - erc20.main(); - assert_eq!(Allowance::get(&erc20.sdk, owner, spender), U256::from(1000)); - sdk.take_output(); - - // check allowance - let allowance_call = AllowanceCall::new((owner, spender)).encode(); - let sdk = sdk.with_input(allowance_call); - erc20.main(); - let result = sdk.take_output(); - let allowance = U256::from_be_slice(&result); - assert_eq!(allowance, U256::from(1000)); + }); + + let mut contract = ERC20::new(sdk.clone(), U256::from(0), 0); + contract.deploy(); + + let balance_of_input = BalanceOfCall::new((deployer,)).encode(); + println!("{:x}", &balance_of_input); + + println!( + "Address IS_DYNAMIC: {}", +
>::IS_DYNAMIC + ); + + // Check if the tuple (Address,) is dynamic + println!( + "(Address,) IS_DYNAMIC: {}", + <(Address,) as Encoder>::IS_DYNAMIC + ); + + let decoded = BalanceOfCall::decode(&&balance_of_input.chunk()[4..]).unwrap(); + + println!("{:?}", decoded); } - #[serial] #[test] - pub fn test_transfer_from() { - let owner = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); - let spender = address!("390a4CEdBb65be7511D9E1a35b115376F39DbDF3"); - let recipient = address!("6dDb6e7F3b7e4991e3f75121aE3De2e1edE3bF19"); + fn test_transfer_functionality() { + // Arrange + let sender = address!("1111111111111111111111111111111111111111"); + let recipient = address!("2222222222222222222222222222222222222222"); + let token_address = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); + let initial_supply = U256::from(1_000_000); + let transfer_amount = U256::from(100_000); + + let constructor_call = + ConstructorCall::new(("TestToken".to_string(), "TST".to_string(), initial_supply)); + + let sdk = HostTestingContext::default() + .with_input(constructor_call.encode()) + .with_contract_context(ContractContextV1 { + address: token_address, + caller: sender, + ..Default::default() + }); - let sdk = HostTestingContext::default().with_contract_context(ContractContextV1 { - caller: owner, - ..Default::default() - }); - let mut erc20 = ERC20::new(sdk.clone()); + let mut contract = ERC20::new(sdk.clone(), U256::from(0), 0); + contract.deploy(); + + // Act - Transfer tokens from sender to recipient + contract.sdk = contract + .sdk + .with_input(TransferCall::new((recipient, transfer_amount)).encode()) + .with_contract_context(ContractContextV1 { + address: token_address, + caller: sender, + ..Default::default() + }); + + contract.main(); + let transfer_result = TransferReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); + + // Assert - Check return value + assert_eq!( + transfer_result.0 .0, + U256::from(1), + "transfer should return 1 on success" + ); - // Deploy contract and approve allowance - erc20.deploy(); + // Verify sender balance decreased + contract.sdk = contract + .sdk + .with_input(BalanceOfCall::new((sender,)).encode()); + contract.main(); + let sender_balance = BalanceOfReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); + assert_eq!( + sender_balance.0 .0, + initial_supply - transfer_amount, + "sender balance should decrease by transfer amount" + ); + // Verify recipient balance increased + contract.sdk = contract + .sdk + .with_input(BalanceOfCall::new((recipient,)).encode()); + contract.main(); + let recipient_balance = BalanceOfReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); assert_eq!( - Balance::get(&mut erc20.sdk, owner).to_string(), - "1000000000000000000000000" + recipient_balance.0 .0, transfer_amount, + "recipient balance should equal transfer amount" ); - let approve_call = ApproveCall::new((spender, U256::from(1000))).encode(); - let sdk = sdk.with_input(approve_call); - erc20.main(); + // Test edge case: transfer zero amount + contract.sdk = contract + .sdk + .with_input(TransferCall::new((recipient, U256::ZERO)).encode()) + .with_contract_context(ContractContextV1 { + address: token_address, + caller: sender, + ..Default::default() + }); + + contract.main(); + let zero_transfer_result = + TransferReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); + assert_eq!( + zero_transfer_result.0 .0, + U256::from(1), + "transfer of 0 should succeed" + ); - // Transfer from owner to recipient via spender - let transfer_from_call = - TransferFromCall::new((owner, recipient, U256::from(100))).encode(); - sdk.with_input(transfer_from_call) + // Get actual current balance before transferring all + contract.sdk = contract + .sdk + .with_input(BalanceOfCall::new((sender,)).encode()); + contract.main(); + let current_sender_balance = BalanceOfReturn::decode(&&contract.sdk.take_output()[..]) + .unwrap() + .0 + .0; + + // Test transfer entire balance using the actual current balance + contract.sdk = contract + .sdk + .with_input(TransferCall::new((recipient, current_sender_balance)).encode()) .with_contract_context(ContractContextV1 { - caller: spender, + address: token_address, + caller: sender, ..Default::default() }); - erc20.main(); - // Check balances and allowance + contract.main(); + let entire_balance_transfer_result = + TransferReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); assert_eq!( - Balance::get(&mut erc20.sdk, owner).to_string(), - "999999999999999999999900" + entire_balance_transfer_result.0 .0, + U256::from(1), + "transfer of 0 should succeed" ); - assert_eq!(Balance::get(&mut erc20.sdk, recipient).to_string(), "100"); + + // Verify sender now has 0 + contract.sdk = contract + .sdk + .with_input(BalanceOfCall::new((sender,)).encode()); + contract.main(); + let output = contract.sdk.take_output(); + let final_sender_balance = BalanceOfReturn::decode(&&output[..]).unwrap(); + assert_eq!( - Allowance::get(&mut erc20.sdk, owner, spender).to_string(), - "900" + final_sender_balance.0 .0, + U256::ZERO, + "sender should have zero balance after transferring all" ); } + + #[test] + fn print_constructor_params_hex() { + let constructor_params = ConstructorCall::new(( + "TestToken".to_string(), + "TST".to_string(), + U256::from(1_000_000), + )) + .encode(); + println!("Constructor input: {:x}", &constructor_params); + } } From 3fd821aeec5be62d1e1b13cc4147ae39cca93622 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Sat, 13 Sep 2025 13:12:41 +0400 Subject: [PATCH 12/16] feat(storage): add primitive codec support for alloy types Add PrimitiveCodec implementations for Uint, Signed, and FixedBytes types from alloy_primitives. Also add support for Rust primitive signed integers (i8-i128) and u128/usize. Remove redundant MapKey implementations for signed integers as they're now covered by the generic PrimitiveCodec impl. - Implement PrimitiveCodec for Uint (8-256 bits) - Implement PrimitiveCodec for Signed (8-256 bits) - Implement PrimitiveCodec for FixedBytes (1-32 bytes) - Add support for Rust primitive types: i8-i128, u128, usize, isize - Add comprehensive test coverage for all new types --- crates/sdk/src/storage/map.rs | 30 -- crates/sdk/src/storage/primitive.rs | 423 ++++++++++++++++++++++++---- 2 files changed, 376 insertions(+), 77 deletions(-) diff --git a/crates/sdk/src/storage/map.rs b/crates/sdk/src/storage/map.rs index 3d61b54e7..625bc5898 100644 --- a/crates/sdk/src/storage/map.rs +++ b/crates/sdk/src/storage/map.rs @@ -134,36 +134,6 @@ impl MapKey for String { } } -// Implement for signed integers -impl MapKey for i64 { - fn compute_slot(&self, root_slot: U256) -> U256 { - // Two's complement representation - let value = U256::from(*self as u64); - value.compute_slot(root_slot) - } -} - -impl MapKey for i32 { - fn compute_slot(&self, root_slot: U256) -> U256 { - let value = U256::from(*self as u32); - value.compute_slot(root_slot) - } -} - -impl MapKey for i16 { - fn compute_slot(&self, root_slot: U256) -> U256 { - let value = U256::from(*self as u16); - value.compute_slot(root_slot) - } -} - -impl MapKey for i8 { - fn compute_slot(&self, root_slot: U256) -> U256 { - let value = U256::from(*self as u8); - value.compute_slot(root_slot) - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/sdk/src/storage/primitive.rs b/crates/sdk/src/storage/primitive.rs index 20fc7ea8d..569bafb7a 100644 --- a/crates/sdk/src/storage/primitive.rs +++ b/crates/sdk/src/storage/primitive.rs @@ -2,7 +2,7 @@ use crate::storage::{ PrimitiveAccess, PrimitiveCodec, StorageDescriptor, StorageLayout, StorageOps, }; use core::marker::PhantomData; -use fluentbase_types::{Address, StorageAPI, U256}; +use fluentbase_types::{Address, FixedBytes, Signed, StorageAPI, Uint, U256}; // --- 1. Descriptor --- @@ -93,18 +93,6 @@ impl StorageLayout for StoragePrimitive { // --- 5. StorageCodec Implementations for Core Primitives --- -impl PrimitiveCodec for U256 { - const ENCODED_SIZE: usize = 32; - - fn encode_into(&self, target: &mut [u8]) { - target.copy_from_slice(&self.to_be_bytes::<32>()); - } - - fn decode(bytes: &[u8]) -> Self { - U256::from_be_bytes::<32>(bytes.try_into().unwrap()) - } -} - impl PrimitiveCodec for Address { const ENCODED_SIZE: usize = 20; @@ -129,52 +117,199 @@ impl PrimitiveCodec for bool { } } -impl PrimitiveCodec for u64 { - const ENCODED_SIZE: usize = 8; - - fn encode_into(&self, target: &mut [u8]) { - target.copy_from_slice(&self.to_be_bytes()); - } - - fn decode(bytes: &[u8]) -> Self { - u64::from_be_bytes(bytes.try_into().unwrap()) - } +// Macro for alloy_primitives::Uint types +macro_rules! impl_uint_codec { + ($($bits:literal => $limbs:literal),* $(,)?) => { + $( + impl PrimitiveCodec for Uint<$bits, $limbs> { + const ENCODED_SIZE: usize = $bits / 8; + + fn encode_into(&self, target: &mut [u8]) { + debug_assert_eq!(target.len(), Self::ENCODED_SIZE); + let bytes = self.to_be_bytes::<{ $bits / 8 }>(); + target.copy_from_slice(&bytes); + } + + fn decode(bytes: &[u8]) -> Self { + debug_assert_eq!(bytes.len(), Self::ENCODED_SIZE); + Self::from_be_bytes::<{ $bits / 8 }>(bytes.try_into().unwrap()) + } + } + )* + }; } -impl PrimitiveCodec for u32 { - const ENCODED_SIZE: usize = 4; +// Macro for alloy_primitives::Signed types +macro_rules! impl_signed_codec { + ($($bits:literal => $limbs:literal),* $(,)?) => { + $( + impl PrimitiveCodec for Signed<$bits, $limbs> { + const ENCODED_SIZE: usize = $bits / 8; + + fn encode_into(&self, target: &mut [u8]) { + debug_assert_eq!(target.len(), Self::ENCODED_SIZE); + let bytes = self.to_be_bytes::<{ $bits / 8 }>(); + target.copy_from_slice(&bytes); + } + + fn decode(bytes: &[u8]) -> Self { + debug_assert_eq!(bytes.len(), Self::ENCODED_SIZE); + Self::from_be_bytes::<{ $bits / 8 }>(bytes.try_into().unwrap()) + } + } + )* + }; +} - fn encode_into(&self, target: &mut [u8]) { - target.copy_from_slice(&self.to_be_bytes()); - } +// Macro for Rust unsigned integer types (excluding those already implemented) +macro_rules! impl_rust_uint_codec { + ($($ty:ty),* $(,)?) => { + $( + impl PrimitiveCodec for $ty { + const ENCODED_SIZE: usize = core::mem::size_of::<$ty>(); + + fn encode_into(&self, target: &mut [u8]) { + debug_assert_eq!(target.len(), Self::ENCODED_SIZE); + target.copy_from_slice(&self.to_be_bytes()); + } + + fn decode(bytes: &[u8]) -> Self { + debug_assert_eq!(bytes.len(), Self::ENCODED_SIZE); + <$ty>::from_be_bytes(bytes.try_into().unwrap()) + } + } + )* + }; +} - fn decode(bytes: &[u8]) -> Self { - u32::from_be_bytes(bytes.try_into().unwrap()) - } +// Macro for Rust signed integer types +macro_rules! impl_rust_sint_codec { + ($($ty:ty),* $(,)?) => { + $( + impl PrimitiveCodec for $ty { + const ENCODED_SIZE: usize = core::mem::size_of::<$ty>(); + + fn encode_into(&self, target: &mut [u8]) { + debug_assert_eq!(target.len(), Self::ENCODED_SIZE); + target.copy_from_slice(&self.to_be_bytes()); + } + + fn decode(bytes: &[u8]) -> Self { + debug_assert_eq!(bytes.len(), Self::ENCODED_SIZE); + <$ty>::from_be_bytes(bytes.try_into().unwrap()) + } + } + )* + }; } -impl PrimitiveCodec for u16 { - const ENCODED_SIZE: usize = 2; +// Macro for alloy_primitives::FixedBytes types +macro_rules! impl_fixed_bytes_codec { + ($($bytes:literal),* $(,)?) => { + $( + impl PrimitiveCodec for FixedBytes<$bytes> { + const ENCODED_SIZE: usize = $bytes; + + fn encode_into(&self, target: &mut [u8]) { + debug_assert_eq!(target.len(), Self::ENCODED_SIZE); + target.copy_from_slice(self.as_slice()); + } + + fn decode(bytes: &[u8]) -> Self { + debug_assert_eq!(bytes.len(), Self::ENCODED_SIZE); + FixedBytes::from_slice(bytes) + } + } + )* + }; +} - fn encode_into(&self, target: &mut [u8]) { - target.copy_from_slice(&self.to_be_bytes()); - } +// Apply macros for all Uint types (LIMBS = (BITS + 63) / 64) +impl_uint_codec! { + 8 => 1, + 16 => 1, + 24 => 1, + 32 => 1, + 40 => 1, + 48 => 1, + 56 => 1, + 64 => 1, + 72 => 2, + 80 => 2, + 88 => 2, + 96 => 2, + 104 => 2, + 112 => 2, + 120 => 2, + 128 => 2, + 136 => 3, + 144 => 3, + 152 => 3, + 160 => 3, + 168 => 3, + 176 => 3, + 184 => 3, + 192 => 3, + 200 => 4, + 208 => 4, + 216 => 4, + 224 => 4, + 232 => 4, + 240 => 4, + 248 => 4, + 256 => 4, +} - fn decode(bytes: &[u8]) -> Self { - u16::from_be_bytes(bytes.try_into().unwrap()) - } +// Apply macros for all Signed types +impl_signed_codec! { + 8 => 1, + 16 => 1, + 24 => 1, + 32 => 1, + 40 => 1, + 48 => 1, + 56 => 1, + 64 => 1, + 72 => 2, + 80 => 2, + 88 => 2, + 96 => 2, + 104 => 2, + 112 => 2, + 120 => 2, + 128 => 2, + 136 => 3, + 144 => 3, + 152 => 3, + 160 => 3, + 168 => 3, + 176 => 3, + 184 => 3, + 192 => 3, + 200 => 4, + 208 => 4, + 216 => 4, + 224 => 4, + 232 => 4, + 240 => 4, + 248 => 4, + 256 => 4, } -impl PrimitiveCodec for u8 { - const ENCODED_SIZE: usize = 1; +// Apply macros for Rust primitive types +impl_rust_uint_codec! { + u8, u16, u32, u64, u128, usize +} - fn encode_into(&self, target: &mut [u8]) { - target[0] = *self; - } +impl_rust_sint_codec! { + i8, i16, i32, i64, i128, isize +} - fn decode(bytes: &[u8]) -> Self { - bytes[0] - } +impl_fixed_bytes_codec! { + 1, 2, 3, 4, 5, 6, 7, 8, + 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, } #[cfg(test)] @@ -296,4 +431,198 @@ mod tests { assert_eq!(u16_accessor.get(&sdk), 0xABCD); assert_eq!(u32_accessor.get(&sdk), 0x12345678); } + + #[test] + fn test_uint_types() { + let mut sdk = MockStorage::new(); + + // Test U8 (Uint<8, 1>) + let u8_accessor = > as StorageLayout>::access( + StoragePrimitive::>::new(U256::from(10), 31), + ); + u8_accessor.set(&mut sdk, Uint::<8, 1>::from(255)); + assert_eq!(u8_accessor.get(&sdk), Uint::<8, 1>::from(255)); + + // Test U128 (Uint<128, 2>) + let u128_accessor = + > as StorageLayout>::access(StoragePrimitive::< + Uint<128, 2>, + >::new( + U256::from(11), 16 + )); + let u128_val = Uint::<128, 2>::from(0x12345678_9ABCDEF0_u128); + u128_accessor.set(&mut sdk, u128_val); + assert_eq!(u128_accessor.get(&sdk), u128_val); + } + + #[test] + fn test_signed_types() { + let mut sdk = MockStorage::new(); + + // Test I8 (Signed<8, 1>) + let i8_accessor = + > as StorageLayout>::access(StoragePrimitive::< + Signed<8, 1>, + >::new( + U256::from(20), 31 + )); + i8_accessor.set(&mut sdk, Signed::<8, 1>::try_from(-42).unwrap()); + assert_eq!( + i8_accessor.get(&sdk), + Signed::<8, 1>::try_from(-42).unwrap() + ); + + // Test I256 with negative value + let i256_accessor = + > as StorageLayout>::access(StoragePrimitive::< + Signed<256, 4>, + >::new( + U256::from(21), 0 + )); + let negative_val = Signed::<256, 4>::try_from(-1000000).unwrap(); + i256_accessor.set(&mut sdk, negative_val); + assert_eq!(i256_accessor.get(&sdk), negative_val); + } + + #[test] + fn test_fixed_bytes_types() { + let mut sdk = MockStorage::new(); + + // Test FixedBytes<1> (bytes1) + let bytes1_accessor = + > as StorageLayout>::access(StoragePrimitive::< + FixedBytes<1>, + >::new( + U256::from(30), 31 + )); + let bytes1_val = FixedBytes::<1>::from([0xAB]); + bytes1_accessor.set(&mut sdk, bytes1_val); + assert_eq!(bytes1_accessor.get(&sdk), bytes1_val); + + // Test FixedBytes<20> (bytes20 - address size) + let bytes20_accessor = + > as StorageLayout>::access(StoragePrimitive::< + FixedBytes<20>, + >::new( + U256::from(31), 12 + )); + let bytes20_val = FixedBytes::<20>::from([0x42; 20]); + bytes20_accessor.set(&mut sdk, bytes20_val); + assert_eq!(bytes20_accessor.get(&sdk), bytes20_val); + + // Test FixedBytes<32> (bytes32 - full slot) + let bytes32_accessor = + > as StorageLayout>::access(StoragePrimitive::< + FixedBytes<32>, + >::new( + U256::from(32), 0 + )); + let bytes32_val = FixedBytes::<32>::from([0xFF; 32]); + bytes32_accessor.set(&mut sdk, bytes32_val); + assert_eq!(bytes32_accessor.get(&sdk), bytes32_val); + } + + #[test] + fn test_rust_primitive_signed() { + let mut sdk = MockStorage::new(); + + // Test i8 + let i8_accessor = as StorageLayout>::access( + StoragePrimitive::::new(U256::from(40), 31), + ); + i8_accessor.set(&mut sdk, -128i8); + assert_eq!(i8_accessor.get(&sdk), -128i8); + + // Test i64 + let i64_accessor = as StorageLayout>::access( + StoragePrimitive::::new(U256::from(41), 24), + ); + i64_accessor.set(&mut sdk, -9223372036854775808i64); // i64::MIN + assert_eq!(i64_accessor.get(&sdk), -9223372036854775808i64); + } + + #[test] + fn test_rust_primitive_unsigned() { + let mut sdk = MockStorage::new(); + + // Test u128 + let u128_accessor = as StorageLayout>::access(StoragePrimitive::< + u128, + >::new( + U256::from(50), 16 + )); + u128_accessor.set(&mut sdk, 340282366920938463463374607431768211455u128); // u128::MAX + assert_eq!( + u128_accessor.get(&sdk), + 340282366920938463463374607431768211455u128 + ); + } + + #[test] + fn test_mixed_packing() { + let mut sdk = MockStorage::new(); + let slot = U256::from(60); + + // Pack: I8 | U16 | FixedBytes<4> | bool + // Offsets: 31 | 29 | 25 | 24 + + let i8_accessor = + > as StorageLayout>::access(StoragePrimitive::< + Signed<8, 1>, + >::new(slot, 31)); + let u16_accessor = > as StorageLayout>::access( + StoragePrimitive::>::new(slot, 29), + ); + let bytes4_accessor = + > as StorageLayout>::access(StoragePrimitive::< + FixedBytes<4>, + >::new(slot, 25)); + let bool_accessor = + as StorageLayout>::access(StoragePrimitive::::new( + slot, 24, + )); + + // Set values + i8_accessor.set(&mut sdk, Signed::<8, 1>::try_from(-1).unwrap()); + u16_accessor.set(&mut sdk, Uint::<16, 1>::from(0xBEEF)); + bytes4_accessor.set(&mut sdk, FixedBytes::<4>::from([0xDE, 0xAD, 0xBE, 0xEF])); + bool_accessor.set(&mut sdk, true); + + // Verify all values remain correct + assert_eq!(i8_accessor.get(&sdk), Signed::<8, 1>::try_from(-1).unwrap()); + assert_eq!(u16_accessor.get(&sdk), Uint::<16, 1>::from(0xBEEF)); + assert_eq!( + bytes4_accessor.get(&sdk), + FixedBytes::<4>::from([0xDE, 0xAD, 0xBE, 0xEF]) + ); + assert!(bool_accessor.get(&sdk)); + } + + #[test] + fn test_edge_cases() { + let mut sdk = MockStorage::new(); + + // Test signed MIN and MAX values + let i128_accessor = + > as StorageLayout>::access(StoragePrimitive::< + Signed<128, 2>, + >::new( + U256::from(70), 16 + )); + + // Test MIN + let min_val = Signed::<128, 2>::MIN; + i128_accessor.set(&mut sdk, min_val); + assert_eq!(i128_accessor.get(&sdk), min_val); + + // Test MAX + let max_val = Signed::<128, 2>::MAX; + i128_accessor.set(&mut sdk, max_val); + assert_eq!(i128_accessor.get(&sdk), max_val); + + // Test -1 (all bits set) + let neg_one = Signed::<128, 2>::try_from(-1).unwrap(); + i128_accessor.set(&mut sdk, neg_one); + assert_eq!(i128_accessor.get(&sdk), neg_one); + } } From 49c95c515d736903c4c122e5d53025ce35b42047 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Mon, 15 Sep 2025 10:56:11 +0400 Subject: [PATCH 13/16] refactor(storage): simplify storage API and improve type ergonomics Refactor storage module to simplify the API and remove unnecessary type bounds. Major breaking changes to constants, traits, and vector operations to provide a cleaner and more ergonomic interface for storage operations. - Rename StorageLayout constants: REQUIRED_SLOTS to SLOTS, ENCODED_SIZE to BYTES - Rename PrimitiveCodec trait to PackableCodec for clarity - Remove composite module and integrate functionality directly - Add manual Copy/Clone implementations to avoid unnecessary T: Copy bounds - Change vector API: push/pop to grow/shrink for complex types - Add specialized push/pop methods for vectors of primitive types --- crates/sdk/src/lib.rs | 2 +- crates/sdk/src/storage/array.rs | 257 ++++-------- crates/sdk/src/storage/bytes.rs | 616 +++++++++++----------------- crates/sdk/src/storage/composite.rs | 70 ---- crates/sdk/src/storage/map.rs | 157 ++++--- crates/sdk/src/storage/mod.rs | 262 +++++------- crates/sdk/src/storage/primitive.rs | 583 ++++++++------------------ crates/sdk/src/storage/vec.rs | 354 +++++++--------- 8 files changed, 817 insertions(+), 1484 deletions(-) delete mode 100644 crates/sdk/src/storage/composite.rs diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 1023f8ba1..880469a5a 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -14,9 +14,9 @@ pub mod panic; #[cfg(not(feature = "std"))] pub mod rwasm; pub mod shared; +pub mod storage; #[deprecated(note = "Use `fluentbase_sdk::storage` instead", since = "0.4.5-dev")] pub mod storage_legacy; -pub mod storage; pub use allocator::*; pub use fluentbase_codec as codec; diff --git a/crates/sdk/src/storage/array.rs b/crates/sdk/src/storage/array.rs index 2fe11139a..ae44112a6 100644 --- a/crates/sdk/src/storage/array.rs +++ b/crates/sdk/src/storage/array.rs @@ -1,27 +1,37 @@ -use crate::{ - storage::{ArrayAccess, StorageDescriptor, StorageLayout}, - U256, -}; +use crate::storage::{StorageDescriptor, StorageLayout, U256}; use core::marker::PhantomData; -// --- 1. Array Descriptor --- - -/// A descriptor for a fixed-size array in storage. -/// Arrays always start at slot boundaries (offset = 0). -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Fixed-size array in storage. +/// Arrays always start at slot boundaries. +#[derive(Debug, PartialEq, Eq)] pub struct StorageArray { base_slot: U256, _marker: PhantomData, } +// Manually implement Copy/Clone to avoid unnecessary T: Copy bound. +// StorageArray is just a descriptor (slot + phantom marker), not actual data. +// PhantomData is always Copy regardless of T, so the descriptor can be Copy +// even when T represents non-Copy types like Vec or String. +impl Clone for StorageArray { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for StorageArray {} + impl StorageArray { - /// Creates a new array descriptor at the given base slot. pub const fn new(base_slot: U256) -> Self { Self { base_slot, _marker: PhantomData, } } + + pub const fn base_slot(&self) -> U256 { + self.base_slot + } } impl StorageDescriptor for StorageArray { @@ -42,67 +52,62 @@ impl StorageDescriptor for StorageArray { } } -// --- 2. ArrayAccess Implementation --- - -impl ArrayAccess for StorageArray +impl StorageArray where T::Descriptor: StorageDescriptor, { - fn at(&self, index: usize) -> T::Accessor { - assert!(index < N, "array index out of bounds"); - let (slot, offset) = if T::ENCODED_SIZE <= 32 && T::REQUIRED_SLOTS == 0 { - let element_size = T::ENCODED_SIZE; - let elements_per_slot = 32 / element_size; + /// Calculate how many elements fit in one slot (for packable types). + #[inline] + fn elements_per_slot() -> usize { + if T::SLOTS == 0 { + 32 / T::BYTES + } else { + 1 + } + } + /// Calculate storage location for element at index. + fn element_location(&self, index: usize) -> (U256, u8) { + if T::SLOTS == 0 { + // Packable: multiple elements per slot + let elements_per_slot = Self::elements_per_slot(); let slot_index = index / elements_per_slot; let position_in_slot = index % elements_per_slot; - // Solidity packs from right to left - // the First element goes to the rightmost position - // offset = start position from left edge - // For the element at position 0 (rightmost): offset = 32 - element_size - // For the element at position 1: offset = 32-2*element_size - let offset = (32 - (position_in_slot + 1) * element_size) as u8; + // Pack from right to left (Solidity convention) + let offset = (32 - (position_in_slot + 1) * T::BYTES) as u8; (self.base_slot + U256::from(slot_index), offset) } else { - // Non-packable types - let slots = if T::REQUIRED_SLOTS == 0 { - 1 - } else { - T::REQUIRED_SLOTS - }; - (self.base_slot + U256::from(index * slots), 0) - }; + // Non-packable: each element uses T::SLOTS slots + (self.base_slot + U256::from(index * T::SLOTS), 0) + } + } + /// Access element at index. + pub fn at(&self, index: usize) -> T::Accessor { + assert!(index < N, "array index out of bounds"); + let (slot, offset) = self.element_location(index); T::access(T::Descriptor::new(slot, offset)) } } - -// --- 3. StorageLayout Implementation --- - -impl StorageLayout for StorageArray -where - T::Descriptor: StorageDescriptor, -{ - type Descriptor = StorageArray; - - // Array acts as its own accessor - no separate accessor type needed +impl StorageLayout for StorageArray { + type Descriptor = Self; type Accessor = Self; - const REQUIRED_SLOTS: usize = { - if T::REQUIRED_SLOTS == 0 { - let elements_per_slot = 32 / T::ENCODED_SIZE; - N.div_ceil(elements_per_slot) - } else { - N * T::REQUIRED_SLOTS - } + const BYTES: usize = if T::SLOTS == 0 { + T::BYTES * N + } else { + T::SLOTS * N * 32 }; - const ENCODED_SIZE: usize = Self::REQUIRED_SLOTS * 32; + const SLOTS: usize = if T::SLOTS == 0 { + (T::BYTES * N + 31) / 32 + } else { + T::SLOTS * N + }; fn access(descriptor: Self::Descriptor) -> Self::Accessor { - // Return the descriptor itself as it acts as the accessor descriptor } } @@ -110,154 +115,56 @@ where #[cfg(test)] mod tests { use super::*; - use crate::storage::mock::MockStorage; - use crate::storage::primitive::StoragePrimitive; - use crate::storage::PrimitiveAccess; - - #[test] - fn test_u256_array_no_packing() { - let mut sdk = MockStorage::new(); - let array = StorageArray::, 3>::new(U256::from(10)); - - // Each U256 takes a full slot - no packing possible - assert_eq!( - , 3> as StorageLayout>::REQUIRED_SLOTS, - 3 - ); - - // Direct array access without intermediate accessor - array.at(0).set(&mut sdk, U256::from(100)); - array.at(1).set(&mut sdk, U256::from(200)); - array.at(2).set(&mut sdk, U256::from(300)); - - // Verify each value is in its own slot - assert_eq!(sdk.get_slot(U256::from(10)), U256::from(100)); - assert_eq!(sdk.get_slot(U256::from(11)), U256::from(200)); - assert_eq!(sdk.get_slot(U256::from(12)), U256::from(300)); - } + use crate::storage::{ + mock::MockStorage, + primitive::{StorageU256, StorageU64}, + }; #[test] - fn test_u64_array_packing() { + fn test_packing_correctness() { + // Critical: verify array packing follows Solidity convention (right to left) let mut sdk = MockStorage::new(); - let array = StorageArray::, 4>::new(U256::from(20)); - - // 4 u64 values (8 bytes each) should pack into 1 slot (32 bytes) - assert_eq!( - , 4> as StorageLayout>::REQUIRED_SLOTS, - 1 - ); + let array = StorageArray::::new(U256::from(10)); + // Set 5 u64 values (should use 2 slots: 4 in first, 1 in second) array.at(0).set(&mut sdk, 0x1111111111111111u64); array.at(1).set(&mut sdk, 0x2222222222222222u64); array.at(2).set(&mut sdk, 0x3333333333333333u64); array.at(3).set(&mut sdk, 0x4444444444444444u64); + array.at(4).set(&mut sdk, 0x5555555555555555u64); - // All values packed in slot 20, from right to left - let expected = "4444444444444444333333333333333322222222222222221111111111111111"; - assert_eq!(sdk.get_slot_hex(U256::from(20)), expected); - } - - #[test] - fn test_bool_array_packing() { - let mut sdk = MockStorage::new(); - let array = StorageArray::, 32>::new(U256::from(30)); - - // 32 bools (1 byte each) should pack into 1 slot + // Verify packing: elements 0-3 in slot 10, element 4 in slot 11 assert_eq!( - , 32> as StorageLayout>::REQUIRED_SLOTS, - 1 + sdk.get_slot_hex(U256::from(10)), + "4444444444444444333333333333333322222222222222221111111111111111" + ); + assert_eq!( + sdk.get_slot_hex(U256::from(11)), + "0000000000000000000000000000000000000000000000005555555555555555" ); - - // Set some values - array.at(0).set(&mut sdk, true); - array.at(1).set(&mut sdk, false); - array.at(2).set(&mut sdk, true); - array.at(31).set(&mut sdk, true); - - // Read back - assert!(array.at(0).get(&sdk)); - assert!(!array.at(1).get(&sdk)); - assert!(array.at(2).get(&sdk)); - assert!(array.at(31).get(&sdk)); } #[test] - fn test_nested_array() { + fn test_nested_arrays() { + // Critical: verify nested arrays calculate slots correctly let mut sdk = MockStorage::new(); - // Array of arrays: 3 arrays, each containing 2 U256 values - let array = StorageArray::, 2>, 3>::new(U256::from(50)); + let array = StorageArray::, 3>::new(U256::from(50)); - // Should take 6 slots total (3 arrays * 2 slots each) - assert_eq!( - , 2>, 3> as StorageLayout>::REQUIRED_SLOTS, - 6 - ); + // Should use 6 slots total + assert_eq!(StorageArray::, 3>::SLOTS, 6); - // Access nested elements + // Set and verify nested values array.at(0).at(0).set(&mut sdk, U256::from(100)); - array.at(0).at(1).set(&mut sdk, U256::from(101)); - array.at(1).at(0).set(&mut sdk, U256::from(200)); - array.at(1).at(1).set(&mut sdk, U256::from(201)); - array.at(2).at(0).set(&mut sdk, U256::from(300)); array.at(2).at(1).set(&mut sdk, U256::from(301)); - // Verify storage layout - sequential slots for nested arrays - assert_eq!(sdk.get_slot(U256::from(50)), U256::from(100)); // array[0][0] - assert_eq!(sdk.get_slot(U256::from(51)), U256::from(101)); // array[0][1] - assert_eq!(sdk.get_slot(U256::from(52)), U256::from(200)); // array[1][0] - assert_eq!(sdk.get_slot(U256::from(53)), U256::from(201)); // array[1][1] - assert_eq!(sdk.get_slot(U256::from(54)), U256::from(300)); // array[2][0] - assert_eq!(sdk.get_slot(U256::from(55)), U256::from(301)); // array[2][1] + assert_eq!(sdk.get_slot(U256::from(50)), U256::from(100)); + assert_eq!(sdk.get_slot(U256::from(55)), U256::from(301)); } #[test] #[should_panic(expected = "array index out of bounds")] fn test_bounds_check() { - let array = StorageArray::, 3>::new(U256::from(60)); - - // This should panic - index 3 is out of bounds for an array of size 3 - array.at(3); - } - - #[test] - fn test_packed_array_multiple_slots() { - let mut sdk = MockStorage::new(); - let array = StorageArray::, 5>::new(U256::from(70)); - - // 5 u64 values need 2 slots (4 in the first slot, 1 in the second) - assert_eq!( - , 5> as StorageLayout>::REQUIRED_SLOTS, - 2 - ); - - array.at(0).set(&mut sdk, 0x1111111111111111u64); - array.at(1).set(&mut sdk, 0x2222222222222222u64); - array.at(2).set(&mut sdk, 0x3333333333333333u64); - array.at(3).set(&mut sdk, 0x4444444444444444u64); - array.at(4).set(&mut sdk, 0x5555555555555555u64); - - // First 4 packed in slot 70 - let expected_slot_70 = "4444444444444444333333333333333322222222222222221111111111111111"; - assert_eq!(sdk.get_slot_hex(U256::from(70)), expected_slot_70); - - // Fifth element in slot 71 - let expected_slot_71 = "0000000000000000000000000000000000000000000000005555555555555555"; - assert_eq!(sdk.get_slot_hex(U256::from(71)), expected_slot_71); - } - - #[test] - fn test_array_get_operations() { - let mut sdk = MockStorage::new(); - let array = StorageArray::, 8>::new(U256::from(80)); - - // Set values first - for i in 0..8 { - array.at(i).set(&mut sdk, (i as u32 + 1) * 100); - } - - // Test get operations - for i in 0..8 { - assert_eq!(array.at(i).get(&sdk), (i as u32 + 1) * 100); - } + let array = StorageArray::::new(U256::from(0)); + array.at(3); // Should panic } } diff --git a/crates/sdk/src/storage/bytes.rs b/crates/sdk/src/storage/bytes.rs index c23b92fc8..be0e9abd2 100644 --- a/crates/sdk/src/storage/bytes.rs +++ b/crates/sdk/src/storage/bytes.rs @@ -1,26 +1,31 @@ use crate::{ keccak256, - storage::{BytesAccess, StorageDescriptor, StorageLayout, StorageOps}, + storage::{StorageDescriptor, StorageLayout, StorageOps}, B256, U256, }; use alloc::{string::String, vec::Vec}; use core::marker::PhantomData; use fluentbase_types::StorageAPI; -// --- 1. Bytes Descriptor --- - -/// A descriptor for dynamic byte arrays in storage. -/// Follows Solidity's bytes/string storage optimization: -/// - Short (< 32 bytes): data and length*2 stored in base slot +/// Dynamic byte array in storage. +/// Optimized for Solidity compatibility: +/// - Short (< 32 bytes): data and length*2 in base slot /// - Long (≥ 32 bytes): length*2+1 in base slot, data at keccak256(base_slot) -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub struct StorageBytes { base_slot: U256, _marker: PhantomData<()>, } +impl Clone for StorageBytes { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for StorageBytes {} + impl StorageBytes { - /// Creates a new bytes descriptor at the given base slot. pub const fn new(base_slot: U256) -> Self { Self { base_slot, @@ -28,335 +33,229 @@ impl StorageBytes { } } - /// Returns the storage slot for long byte's data. + /// Storage slot for long form data. fn data_slot(&self) -> U256 { let hash = keccak256(self.base_slot.to_be_bytes::<32>()); U256::from_be_bytes(hash.0) } - /// Reads the length and determines storage layout. - /// Returns (length, is_long_form). - fn read_length(&self, sdk: &S) -> (usize, bool) { + /// Read length and check if long form. + fn read_metadata(&self, sdk: &S) -> (usize, bool) { let word = sdk.sload(self.base_slot); let last_byte = word.0[31]; if last_byte & 1 == 0 { - // Short form: length = last_byte / 2 + // Short form ((last_byte / 2) as usize, false) } else { - // Long form: length = (word / 2) - let word_value = U256::from_be_bytes(word.0); - let len = word_value / U256::from(2); + // Long form + let len = U256::from_be_bytes(word.0) / U256::from(2); (len.try_into().unwrap_or(0), true) } } - /// Writes the length to storage. - fn write_length(&self, sdk: &mut S, len: usize) { - if len < 32 { - // Short form: store length * 2 in the last byte - let mut word = sdk.sload(self.base_slot); - word.0[31] = (len * 2) as u8; - sdk.sstore(self.base_slot, word); - } else { - // Long form: store length * 2 + 1 - let len_encoded = U256::from(len * 2 + 1); - sdk.sstore(self.base_slot, B256::from(len_encoded.to_be_bytes::<32>())); - } - } -} - -impl StorageDescriptor for StorageBytes { - fn new(slot: U256, offset: u8) -> Self { - debug_assert_eq!(offset, 0, "bytes always start at slot boundary"); - Self { - base_slot: slot, - _marker: PhantomData, - } - } - - fn slot(&self) -> U256 { - self.base_slot - } - - fn offset(&self) -> u8 { - 0 - } -} - -// --- 2. BytesAccess Implementation --- - -impl BytesAccess for StorageBytes { - fn len(&self, sdk: &S) -> usize { - self.read_length(sdk).0 - } - - fn is_empty(&self, sdk: &S) -> bool { - self.len(sdk) == 0 - } - - fn get(&self, sdk: &S, index: usize) -> u8 { - let (len, is_long) = self.read_length(sdk); - assert!(index < len, "bytes index out of bounds"); - - if !is_long { - // Short form: read from the base slot - sdk.sload(self.base_slot).0[index] - } else { - // Long form: read from data slots - let slot = self.data_slot() + U256::from(index / 32); - sdk.sload(slot).0[index % 32] - } - } - - fn push(&self, sdk: &mut S, byte: u8) { - let (old_len, _) = self.read_length(sdk); - let new_len = old_len + 1; - - if old_len < 31 { - // Still short after adding a byte - let mut word = sdk.sload(self.base_slot); - word.0[old_len] = byte; - word.0[31] = (new_len * 2) as u8; - sdk.sstore(self.base_slot, word); - } else if old_len == 31 { - // Converting from short to long form - let mut word = sdk.sload(self.base_slot); - word.0[31] = byte; - - // Copy data to long storage - sdk.sstore(self.data_slot(), word); - - // Update length to long form - self.write_length(sdk, new_len); - } else { - // Already long form - let slot = self.data_slot() + U256::from(old_len / 32); - let mut word = sdk.sload(slot); - word.0[old_len % 32] = byte; - sdk.sstore(slot, word); - - self.write_length(sdk, new_len); - } - } - - fn pop(&self, sdk: &mut S) -> Option { - let (len, is_long) = self.read_length(sdk); - if len == 0 { - return None; - } - - let index = len - 1; - let byte = self.get(sdk, index); - - if len == 32 && is_long { - // Converting from long to short form - let mut word = sdk.sload(self.data_slot()); - word.0[31] = (index * 2) as u8; - sdk.sstore(self.base_slot, word); - - // Clear the data slot - sdk.sstore(self.data_slot(), B256::ZERO); - } else if !is_long { - // Short form - let mut word = sdk.sload(self.base_slot); - word.0[index] = 0; - word.0[31] = (index * 2) as u8; - sdk.sstore(self.base_slot, word); - } else { - // Long form - just update length - self.write_length(sdk, index); - - // Clear the slot if it's now empty - if index % 32 == 0 && index > 0 { - let slot = self.data_slot() + U256::from(index / 32); - sdk.sstore(slot, B256::ZERO); - } - } - - Some(byte) + /// Get byte array length. + pub fn len(&self, sdk: &S) -> usize { + self.read_metadata(sdk).0 } - fn load(&self, sdk: &S) -> Vec { - let (len, is_long) = self.read_length(sdk); + /// Load entire byte array. + pub fn load(&self, sdk: &S) -> Vec { + let (len, is_long) = self.read_metadata(sdk); let mut result = Vec::with_capacity(len); if !is_long { - // Short form: read from the base slot + // Short form let word = sdk.sload(self.base_slot); result.extend_from_slice(&word.0[..len]); } else { - // Long form: read from data slots + // Long form let data_base = self.data_slot(); - for chunk_start in (0..len).step_by(32) { - let slot = data_base + U256::from(chunk_start / 32); + for i in 0..(len + 31) / 32 { + let slot = data_base + U256::from(i); let word = sdk.sload(slot); - let chunk_end = (chunk_start + 32).min(len); - result.extend_from_slice(&word.0[..(chunk_end - chunk_start)]); + let start = i * 32; + let end = (start + 32).min(len); + result.extend_from_slice(&word.0[..(end - start)]); } } result } - fn store(&self, sdk: &mut S, bytes: &[u8]) { - let new_len = bytes.len(); - let (old_len, was_long) = self.read_length(sdk); + /// Store byte array, replacing existing data. + pub fn store(&self, sdk: &mut S, data: &[u8]) { + let new_len = data.len(); + let (old_len, was_long) = self.read_metadata(sdk); - // Clear old long-form data if necessary + // Clear old long form data if needed if was_long && old_len > 31 { let data_base = self.data_slot(); - for i in 0..old_len.div_ceil(32) { + for i in 0..(old_len + 31) / 32 { sdk.sstore(data_base + U256::from(i), B256::ZERO); } } if new_len < 32 { - // Store in short form + // Store as short form let mut word = B256::ZERO; - word.0[..new_len].copy_from_slice(bytes); + word.0[..new_len].copy_from_slice(data); word.0[31] = (new_len * 2) as u8; sdk.sstore(self.base_slot, word); } else { - // Store in long form + // Store as long form let data_base = self.data_slot(); - // Write data in 32-byte chunks - for (i, chunk) in bytes.chunks(32).enumerate() { + // Write data chunks + for (i, chunk) in data.chunks(32).enumerate() { let mut word = B256::ZERO; word.0[..chunk.len()].copy_from_slice(chunk); sdk.sstore(data_base + U256::from(i), word); } // Write length - self.write_length(sdk, new_len); + let len_encoded = U256::from(new_len * 2 + 1); + sdk.sstore(self.base_slot, B256::from(len_encoded.to_be_bytes::<32>())); + } + } + + /// Read slice of bytes without modifying storage. + pub fn slice(&self, sdk: &S, start: usize, end: usize) -> Vec { + let data = self.load(sdk); + let actual_end = end.min(data.len()); + data.get(start..actual_end) + .map(|s| s.to_vec()) + .unwrap_or_default() + } + + /// Append bytes to existing data. + pub fn append(&self, sdk: &mut S, data: &[u8]) { + let mut current = self.load(sdk); + current.extend_from_slice(data); + self.store(sdk, ¤t); + } + + /// Truncate to specified length. + pub fn truncate(&self, sdk: &mut S, new_len: usize) { + let current = self.load(sdk); + if new_len < current.len() { + self.store(sdk, ¤t[..new_len]); } } - fn clear(&self, sdk: &mut S) { - let (len, is_long) = self.read_length(sdk); + /// Clear all data. + pub fn clear(&self, sdk: &mut S) { + let (len, is_long) = self.read_metadata(sdk); - // Clear data slots if in long form if is_long && len > 31 { let data_base = self.data_slot(); - for i in 0..len.div_ceil(32) { + for i in 0..(len + 31) / 32 { sdk.sstore(data_base + U256::from(i), B256::ZERO); } } - // Clear base slot (sets length to 0) sdk.sstore(self.base_slot, B256::ZERO); } } -// --- 3. StorageLayout Implementation --- +impl StorageDescriptor for StorageBytes { + fn new(slot: U256, offset: u8) -> Self { + debug_assert_eq!(offset, 0, "bytes always start at slot boundary"); + Self::new(slot) + } + + fn slot(&self) -> U256 { + self.base_slot + } + + fn offset(&self) -> u8 { + 0 + } +} impl StorageLayout for StorageBytes { - type Descriptor = StorageBytes; + type Descriptor = Self; type Accessor = Self; - const REQUIRED_SLOTS: usize = 1; - const ENCODED_SIZE: usize = 32; + const BYTES: usize = 32; + const SLOTS: usize = 1; fn access(descriptor: Self::Descriptor) -> Self::Accessor { descriptor } } -// --- 4. StorageString - Simple wrapper around Bytes --- - -/// A descriptor for UTF-8 strings in storage. -/// Simple wrapper around Bytes with string-specific convenience methods. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// UTF-8 string in storage. +#[derive(Debug, PartialEq, Eq)] pub struct StorageString { - bytes: StorageBytes, + base_slot: U256, + _marker: PhantomData<()>, } +impl Clone for StorageString { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for StorageString {} + impl StorageString { - /// Creates a new string descriptor at the given base slot. pub const fn new(base_slot: U256) -> Self { Self { - bytes: StorageBytes::new(base_slot), + base_slot, + _marker: PhantomData, } } - /// Loads the string from storage. - pub fn get_string(&self, sdk: &S) -> String { - let bytes = self.bytes.load(sdk); + /// Get string value. + pub fn get(&self, sdk: &S) -> String { + let bytes = self.as_bytes().load(sdk); String::from_utf8_lossy(&bytes).into_owned() } - /// Stores a string to storage. - pub fn set_string(&self, sdk: &mut S, value: &str) { - self.bytes.store(sdk, value.as_bytes()); + /// Set string value. + pub fn set(&self, sdk: &mut S, value: &str) { + self.as_bytes().store(sdk, value.as_bytes()); } - /// Appends a string slice. - pub fn push_str(&self, sdk: &mut S, string: &str) { - for byte in string.bytes() { - self.bytes.push(sdk, byte); - } + /// Get string length in bytes. + pub fn len(&self, sdk: &S) -> usize { + self.as_bytes().len(sdk) + } + + /// Clear string. + pub fn clear(&self, sdk: &mut S) { + self.as_bytes().clear(sdk); + } + + /// Convert to bytes for complex operations. + pub fn as_bytes(&self) -> StorageBytes { + StorageBytes::new(self.base_slot) } } impl StorageDescriptor for StorageString { fn new(slot: U256, offset: u8) -> Self { debug_assert_eq!(offset, 0, "strings always start at slot boundary"); - Self { - bytes: StorageBytes::new(slot), - } + Self::new(slot) } fn slot(&self) -> U256 { - self.bytes.slot() + self.base_slot } fn offset(&self) -> u8 { - self.bytes.offset() - } -} - -// Delegate BytesAccess to inner Bytes -impl BytesAccess for StorageString { - fn len(&self, sdk: &S) -> usize { - self.bytes.len(sdk) - } - - fn is_empty(&self, sdk: &S) -> bool { - self.bytes.is_empty(sdk) - } - - fn get(&self, sdk: &S, index: usize) -> u8 { - self.bytes.get(sdk, index) - } - - fn push(&self, sdk: &mut S, byte: u8) { - self.bytes.push(sdk, byte); - } - - fn pop(&self, sdk: &mut S) -> Option { - self.bytes.pop(sdk) - } - - fn load(&self, sdk: &S) -> Vec { - self.bytes.load(sdk) - } - - fn store(&self, sdk: &mut S, bytes: &[u8]) { - self.bytes.store(sdk, bytes); - } - - fn clear(&self, sdk: &mut S) { - self.bytes.clear(sdk); + 0 } } impl StorageLayout for StorageString { - type Descriptor = StorageString; + type Descriptor = Self; type Accessor = Self; - const REQUIRED_SLOTS: usize = 1; - const ENCODED_SIZE: usize = 32; + const BYTES: usize = 32; + const SLOTS: usize = 1; fn access(descriptor: Self::Descriptor) -> Self::Accessor { descriptor @@ -369,213 +268,174 @@ mod tests { use crate::storage::mock::MockStorage; #[test] - fn test_short_bytes_storage_layout() { + fn test_bytes_basic_operations() { let mut sdk = MockStorage::new(); let bytes = StorageBytes::new(U256::from(100)); - // Test empty state - assert_eq!(sdk.get_slot_hex(U256::from(100)), "0".repeat(64)); - - // Add 3 bytes - bytes.push(&mut sdk, 0x11); - bytes.push(&mut sdk, 0x22); - bytes.push(&mut sdk, 0x33); - - // Verify storage: data at the beginning, length*2 at the end - let expected = "1122330000000000000000000000000000000000000000000000000000000006"; - assert_eq!(sdk.get_slot_hex(U256::from(100)), expected); - - // Verify API + // Store and load + let data = vec![0x11, 0x22, 0x33]; + bytes.store(&mut sdk, &data); + assert_eq!(bytes.load(&sdk), data); assert_eq!(bytes.len(&sdk), 3); - assert_eq!(bytes.load(&sdk), vec![0x11, 0x22, 0x33]); - } - #[test] - fn test_long_bytes_storage_layout() { - let mut sdk = MockStorage::new(); - let bytes = StorageBytes::new(U256::from(200)); - - // Push exactly 32 bytes - let data: Vec = (0..32).collect(); - for &b in &data { - bytes.push(&mut sdk, b); - } + // Append + bytes.append(&mut sdk, &[0x44, 0x55]); + assert_eq!(bytes.load(&sdk), vec![0x11, 0x22, 0x33, 0x44, 0x55]); - // Verify the base slot contains length*2+1 = 65 = 0x41 - let expected_base = "0000000000000000000000000000000000000000000000000000000000000041"; - assert_eq!(sdk.get_slot_hex(U256::from(200)), expected_base); + // Slice + assert_eq!(bytes.slice(&sdk, 1, 4), vec![0x22, 0x33, 0x44]); + assert!(bytes.slice(&sdk, 10, 20).is_empty()); // Out of bounds - // Verify data is at keccak256(base_slot) - let data_slot = { - let hash = keccak256(U256::from(200).to_be_bytes::<32>()); - U256::from_be_bytes(hash.0) - }; + // Truncate + bytes.truncate(&mut sdk, 3); + assert_eq!(bytes.load(&sdk), vec![0x11, 0x22, 0x33]); - let mut expected_data = String::new(); - for i in 0..32 { - expected_data.push_str(&format!("{i:02x}")); - } - assert_eq!(sdk.get_slot_hex(data_slot), expected_data); + // Clear + bytes.clear(&mut sdk); + assert_eq!(bytes.len(&sdk), 0); } #[test] - fn test_bytes_transition_short_to_long() { + fn test_bytes_form_transitions() { let mut sdk = MockStorage::new(); - let bytes = StorageBytes::new(U256::from(300)); + let bytes = StorageBytes::new(U256::from(200)); - // Fill to 31 bytes (maximum short form) - for i in 0..31 { - bytes.push(&mut sdk, i as u8); - } + // Start with short form (31 bytes max) + let short_data: Vec = (0..31).collect(); + bytes.store(&mut sdk, &short_data); - // Verify short form storage + // Verify short form storage layout let mut expected = String::new(); for i in 0..31 { expected.push_str(&format!("{i:02x}")); } expected.push_str("3e"); // 31*2 = 62 = 0x3e - assert_eq!(sdk.get_slot_hex(U256::from(300)), expected); + assert_eq!(sdk.get_slot_hex(U256::from(200)), expected); - // Push one more byte to trigger the long form - bytes.push(&mut sdk, 31); + // Append to trigger long form (32+ bytes) + bytes.append(&mut sdk, &[31]); + assert_eq!(bytes.len(&sdk), 32); // Verify long form: base slot has length*2+1 = 65 = 0x41 assert_eq!( - sdk.get_slot_hex(U256::from(300)), + sdk.get_slot_hex(U256::from(200)), "0000000000000000000000000000000000000000000000000000000000000041" ); - // Verify data moved to keccak256(base_slot) + // Verify data at keccak256(base_slot) let data_slot = { - let hash = keccak256(U256::from(300).to_be_bytes::<32>()); - U256::from_be_bytes(hash.0) - }; - - let mut expected_data = String::new(); - for i in 0..32 { - expected_data.push_str(&format!("{i:02x}")); - } - assert_eq!(sdk.get_slot_hex(data_slot), expected_data); - } - - #[test] - fn test_bytes_transition_long_to_short() { - let mut sdk = MockStorage::new(); - let bytes = StorageBytes::new(U256::from(400)); - - // Create long form (32 bytes) - for i in 0..32 { - bytes.push(&mut sdk, i as u8); - } - - let data_slot = { - let hash = keccak256(U256::from(400).to_be_bytes::<32>()); + let hash = keccak256(U256::from(200).to_be_bytes::<32>()); U256::from_be_bytes(hash.0) }; + let data = sdk.get_slot_hex(data_slot); + assert_eq!(&data[0..2], "00"); // First byte is 0 + assert_eq!(&data[62..64], "1f"); // Last byte is 31 - // Verify long form - assert_eq!( - sdk.get_slot_hex(U256::from(400)), - "0000000000000000000000000000000000000000000000000000000000000041" - ); + // Truncate back to short form + bytes.truncate(&mut sdk, 30); + assert_eq!(bytes.len(&sdk), 30); - // Pop one byte to trigger the short form - assert_eq!(bytes.pop(&mut sdk), Some(31)); - - // Verify short form restored - let mut expected = String::new(); - for i in 0..31 { - expected.push_str(&format!("{i:02x}")); - } - expected.push_str("3e"); // 31*2 = 62 = 0x3e - assert_eq!(sdk.get_slot_hex(U256::from(400)), expected); - - // Verify data slot cleared + // Verify short form restored and data slot cleared assert_eq!(sdk.get_slot_hex(data_slot), "0".repeat(64)); } #[test] - fn test_bytes_store_and_clear() { + fn test_string_operations() { let mut sdk = MockStorage::new(); - let bytes = StorageBytes::new(U256::from(500)); + let string = StorageString::new(U256::from(300)); - // Store long data - let data: Vec = (100..150).collect(); - bytes.store(&mut sdk, &data); + // Short string + string.set(&mut sdk, "Hello"); + assert_eq!(string.get(&sdk), "Hello"); + assert_eq!(string.len(&sdk), 5); - // Verify length in base slot: 50*2+1 = 101 = 0x65 - assert_eq!( - sdk.get_slot_hex(U256::from(500)), - "0000000000000000000000000000000000000000000000000000000000000065" - ); + // Verify hex storage + let hex = sdk.get_slot_hex(U256::from(300)); + assert_eq!(&hex[0..10], "48656c6c6f"); // "Hello" in hex + assert_eq!(&hex[62..64], "0a"); // length*2 = 10 - // Verify data stored correctly - assert_eq!(bytes.load(&sdk), data); + // Long string (60 chars > 31 bytes) + let long_str = "This is a very long string that exceeds 31 bytes threshold!!"; + string.set(&mut sdk, long_str); + assert_eq!(string.get(&sdk), long_str); + assert_eq!(string.len(&sdk), 60); // Actual length is 60 chars - // Store short data (should clear old slots) - let short_data = vec![0xAA, 0xBB, 0xCC]; - bytes.store(&mut sdk, &short_data); + // Verify long form encoding + let len_encoded = 60 * 2 + 1; // 121 = 0x79 + assert_eq!(sdk.get_slot(U256::from(300)), U256::from(len_encoded)); + + // Complex operations via as_bytes() + let bytes = string.as_bytes(); + let slice = bytes.slice(&sdk, 0, 4); + assert_eq!(slice, b"This"); - // Verify short form + bytes.append(&mut sdk, b" More text."); + assert_eq!(string.len(&sdk), 71); // 60 + 11 = 71 assert_eq!( - sdk.get_slot_hex(U256::from(500)), - "aabbcc0000000000000000000000000000000000000000000000000000000006" + string.get(&sdk), + "This is a very long string that exceeds 31 bytes threshold!! More text." ); - // Clear all - bytes.clear(&mut sdk); - assert_eq!(sdk.get_slot_hex(U256::from(500)), "0".repeat(64)); - assert!(bytes.is_empty(&sdk)); + // Clear + string.clear(&mut sdk); + assert_eq!(string.len(&sdk), 0); } #[test] - fn test_string_storage() { + fn test_bytes_multiple_slots() { let mut sdk = MockStorage::new(); - let string = StorageString::new(U256::from(600)); + let bytes = StorageBytes::new(U256::from(400)); - // Test short string - string.set_string(&mut sdk, "Hello"); - assert_eq!(string.get_string(&sdk), "Hello"); + // Store 100 bytes (requires 4 slots in long form) + let data: Vec = (0..100).collect(); + bytes.store(&mut sdk, &data); - // Verify hex storage - let hex = sdk.get_slot_hex(U256::from(600)); - assert_eq!(&hex[0..10], "48656c6c6f"); // "Hello" in hex - assert_eq!(&hex[62..64], "0a"); // length*2 = 10 + assert_eq!(bytes.load(&sdk), data); + assert_eq!(bytes.len(&sdk), 100); - // Test long string - let long_str = "This is a very long string that will use long form storage!"; - string.set_string(&mut sdk, long_str); - assert_eq!(string.get_string(&sdk), long_str); + // Test slicing across slot boundaries + let slice = bytes.slice(&sdk, 30, 35); + assert_eq!(slice, vec![30, 31, 32, 33, 34]); - // Verify base slot has length encoded - let len_encoded = long_str.len() * 2 + 1; - assert_eq!(sdk.get_slot(U256::from(600)), U256::from(len_encoded)); - } + // Append more data + bytes.append(&mut sdk, &[100, 101, 102]); + assert_eq!(bytes.len(&sdk), 103); - #[test] - #[should_panic(expected = "bytes index out of bounds")] - fn test_bytes_bounds_check() { - let sdk = MockStorage::new(); - let bytes = StorageBytes::new(U256::from(800)); - bytes.get(&sdk, 0); // Should panic - empty bytes + // Truncate to exactly 32 bytes (still long form) + bytes.truncate(&mut sdk, 32); + assert_eq!(bytes.len(&sdk), 32); + + // Verify still in long form + let base_slot_value = sdk.get_slot(U256::from(400)); + assert_eq!(base_slot_value, U256::from(65)); // 32*2+1 = 65 } #[test] - fn test_bytes_multiple_slots() { + fn test_edge_cases() { let mut sdk = MockStorage::new(); - let bytes = StorageBytes::new(U256::from(900)); + let bytes = StorageBytes::new(U256::from(500)); - // Store 100 bytes (requires 4 slots) - let data: Vec = (0..100).collect(); - bytes.store(&mut sdk, &data); + // Empty operations + assert!(bytes.slice(&sdk, 0, 10).is_empty()); + bytes.truncate(&mut sdk, 100); // No-op when empty + assert_eq!(bytes.len(&sdk), 0); - // Verify all data stored and retrieved correctly - assert_eq!(bytes.load(&sdk), data); - assert_eq!(bytes.len(&sdk), 100); + // Single byte + bytes.store(&mut sdk, &[0xFF]); + assert_eq!(bytes.load(&sdk), vec![0xFF]); - // Verify individual bytes - for i in 0..100 { - assert_eq!(bytes.get(&sdk, i), i as u8); - } + // Exactly 31 bytes (max short) + let data_31: Vec = vec![0xAA; 31]; + bytes.store(&mut sdk, &data_31); + assert_eq!(bytes.len(&sdk), 31); + + // Exactly 32 bytes (min long) + let data_32: Vec = vec![0xBB; 32]; + bytes.store(&mut sdk, &data_32); + assert_eq!(bytes.len(&sdk), 32); + + // Verify it's in long form + let base_value = sdk.get_slot(U256::from(500)); + assert_eq!(base_value, U256::from(65)); // 32*2+1 } } diff --git a/crates/sdk/src/storage/composite.rs b/crates/sdk/src/storage/composite.rs deleted file mode 100644 index 374f376e8..000000000 --- a/crates/sdk/src/storage/composite.rs +++ /dev/null @@ -1,70 +0,0 @@ -use crate::{ - storage::{StorageDescriptor, StorageLayout}, - U256, -}; -use core::marker::PhantomData; - -// TODO(d1r1): double-check if we actually need custom Clone, Copy impls here -#[derive(Debug, PartialEq, Eq)] -pub struct Composite { - base_slot: U256, - _phantom: PhantomData, -} - -impl Clone for Composite { - fn clone(&self) -> Self { - Self { - base_slot: self.base_slot, - _phantom: PhantomData, - } - } -} -impl Copy for Composite {} - -impl Composite { - pub const fn new(base_slot: U256) -> Self { - Self { - base_slot, - _phantom: PhantomData, - } - } -} - -impl StorageDescriptor for Composite { - fn new(slot: U256, offset: u8) -> Self { - debug_assert_eq!(offset, 0, "Composite types always start at slot boundary"); - Self::new(slot) - } - - fn slot(&self) -> U256 { - self.base_slot - } - - fn offset(&self) -> u8 { - 0 - } -} - -// ===== CompositeStorage trait ===== - -/// Trait for composite storage types that can be wrapped in Composite -pub trait CompositeStorage: Sized { - /// Number of slots required for this composite type - const REQUIRED_SLOTS: usize; - - /// Create instance from base slot - fn from_slot(base_slot: U256) -> Self; -} - -// Implement StorageLayout for Composite -impl StorageLayout for Composite { - type Descriptor = Self; - type Accessor = T; - - const REQUIRED_SLOTS: usize = T::REQUIRED_SLOTS; - const ENCODED_SIZE: usize = T::REQUIRED_SLOTS * 32; - - fn access(descriptor: Self::Descriptor) -> Self::Accessor { - T::from_slot(descriptor.base_slot) - } -} diff --git a/crates/sdk/src/storage/map.rs b/crates/sdk/src/storage/map.rs index 625bc5898..5a73538fa 100644 --- a/crates/sdk/src/storage/map.rs +++ b/crates/sdk/src/storage/map.rs @@ -1,23 +1,29 @@ -use crate::storage::PrimitiveCodec; use crate::{ keccak256, - storage::{MapAccess, MapKey, StorageDescriptor, StorageLayout}, + storage::{PackableCodec, StorageDescriptor, StorageLayout}, U256, }; use alloc::{string::String, vec::Vec}; use core::marker::PhantomData; -// --- 1. Map Descriptor --- -/// A descriptor for a storage map (mapping in Solidity). -/// Maps don't store data directly at the base slot - it's used only for slot calculation. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Storage map (Solidity mapping). +/// Base slot used only for computing value locations via keccak256. +#[derive(Debug, PartialEq, Eq)] pub struct StorageMap { base_slot: U256, _marker: PhantomData<(K, V)>, } +// Manual Copy/Clone to avoid K,V: Copy bounds +impl Clone for StorageMap { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for StorageMap {} + impl StorageMap { - /// Creates a new map descriptor at the given base slot. pub const fn new(base_slot: U256) -> Self { Self { base_slot, @@ -27,7 +33,8 @@ impl StorageMap { } impl StorageDescriptor for StorageMap { - fn new(slot: U256, _offset: u8) -> Self { + fn new(slot: U256, offset: u8) -> Self { + debug_assert_eq!(offset, 0, "maps always start at slot boundary"); Self::new(slot) } @@ -40,76 +47,72 @@ impl StorageDescriptor for StorageMap { } } -// --- 2. MapAccess Implementation --- - -impl MapAccess for StorageMap +impl StorageMap where V::Descriptor: StorageDescriptor, { - fn entry(&self, key: K) -> V::Accessor { + /// Access value for given key. + pub fn entry(&self, key: K) -> V::Accessor { let value_slot = key.compute_slot(self.base_slot); - // For primitive types smaller than 32 bytes, calculate proper offset - let offset = if V::ENCODED_SIZE < 32 { - (32 - V::ENCODED_SIZE) as u8 + // Packable values start at rightmost position in slot + let offset = if V::SLOTS == 0 { + (32 - V::BYTES) as u8 } else { 0 }; - let value_descriptor = V::Descriptor::new(value_slot, offset); - - V::access(value_descriptor) + V::access(V::Descriptor::new(value_slot, offset)) } } -// --- 3. StorageLayout Implementation --- - impl StorageLayout for StorageMap where V::Descriptor: StorageDescriptor, { - type Descriptor = StorageMap; + type Descriptor = Self; type Accessor = Self; - // Maps only reserve the base slot for computation - const REQUIRED_SLOTS: usize = 1; - const ENCODED_SIZE: usize = 32; + const BYTES: usize = 32; // Base slot only + const SLOTS: usize = 1; // Reserve one slot for hash computation fn access(descriptor: Self::Descriptor) -> Self::Accessor { descriptor } } -// --- 4. MapKey Implementations --- +/// Trait for types that can be used as map keys. +pub trait MapKey { + fn compute_slot(&self, base_slot: U256) -> U256; +} -impl MapKey for T -where - T: PrimitiveCodec, -{ - fn compute_slot(&self, root_slot: U256) -> U256 { +// MapKey for primitive types via PackableCodec +impl MapKey for T { + fn compute_slot(&self, base_slot: U256) -> U256 { let mut key_bytes = [0u8; 32]; - let encoded_size = T::ENCODED_SIZE; - if encoded_size <= 32 { - let offset = 32 - encoded_size; + // Right-align key in 32 bytes + if T::ENCODED_SIZE <= 32 { + let offset = 32 - T::ENCODED_SIZE; self.encode_into(&mut key_bytes[offset..]); } + // keccak256(key || base_slot) let mut data = [0u8; 64]; data[0..32].copy_from_slice(&key_bytes); - data[32..64].copy_from_slice(&root_slot.to_be_bytes::<32>()); + data[32..64].copy_from_slice(&base_slot.to_be_bytes::<32>()); let hash = keccak256(data); U256::from_be_bytes(hash.0) } } +// Dynamic key types impl MapKey for &[u8] { - fn compute_slot(&self, root_slot: U256) -> U256 { - // For dynamic keys: keccak256(key || slot) + fn compute_slot(&self, base_slot: U256) -> U256 { let mut data = Vec::with_capacity(self.len() + 32); data.extend_from_slice(self); - data.extend_from_slice(&root_slot.to_be_bytes::<32>()); + data.extend_from_slice(&base_slot.to_be_bytes::<32>()); let hash = keccak256(&data); U256::from_be_bytes(hash.0) @@ -117,35 +120,33 @@ impl MapKey for &[u8] { } impl MapKey for Vec { - fn compute_slot(&self, root_slot: U256) -> U256 { - self.as_slice().compute_slot(root_slot) + fn compute_slot(&self, base_slot: U256) -> U256 { + self.as_slice().compute_slot(base_slot) } } impl MapKey for &str { - fn compute_slot(&self, root_slot: U256) -> U256 { - self.as_bytes().compute_slot(root_slot) + fn compute_slot(&self, base_slot: U256) -> U256 { + self.as_bytes().compute_slot(base_slot) } } impl MapKey for String { - fn compute_slot(&self, root_slot: U256) -> U256 { - self.as_bytes().compute_slot(root_slot) + fn compute_slot(&self, base_slot: U256) -> U256 { + self.as_bytes().compute_slot(base_slot) } } #[cfg(test)] mod tests { use super::*; - use crate::storage::{ - array::StorageArray, mock::MockStorage, primitive::StoragePrimitive, ArrayAccess, - PrimitiveAccess, - }; + use crate::storage::{array::StorageArray, mock::MockStorage, primitive::StoragePrimitive}; #[test] fn test_map_basic_operations() { let mut sdk = MockStorage::new(); - let map = StorageMap::>::new(U256::from(100)); + let map = + StorageMap::>::new(U256::from(100)); // Set values for different keys map.entry(U256::from(1)).set(&mut sdk, U256::from(111)); @@ -181,21 +182,24 @@ mod tests { let mut sdk = MockStorage::new(); // Bool keys - let bool_map = StorageMap::>::new(U256::from(200)); + let bool_map = + StorageMap::>::new(U256::from(200)); bool_map.entry(true).set(&mut sdk, U256::from(100)); bool_map.entry(false).set(&mut sdk, U256::from(200)); assert_eq!(bool_map.entry(true).get(&sdk), U256::from(100)); assert_eq!(bool_map.entry(false).get(&sdk), U256::from(200)); // String keys - let string_map = StorageMap::<&str, StoragePrimitive>::new(U256::from(300)); + let string_map = + StorageMap::<&str, StoragePrimitive>::new(U256::from(300)); string_map.entry("alice").set(&mut sdk, U256::from(1000)); string_map.entry("bob").set(&mut sdk, U256::from(2000)); assert_eq!(string_map.entry("alice").get(&sdk), U256::from(1000)); assert_eq!(string_map.entry("bob").get(&sdk), U256::from(2000)); // u64 keys - let u64_map = StorageMap::>::new(U256::from(400)); + let u64_map = + StorageMap::>::new(U256::from(400)); u64_map.entry(12345u64).set(&mut sdk, U256::from(999)); assert_eq!(u64_map.entry(12345u64).get(&sdk), U256::from(999)); @@ -205,8 +209,10 @@ mod tests { fn test_nested_maps() { let mut sdk = MockStorage::new(); // Map>> - let map = - StorageMap::>>::new(U256::from(500)); + let map = StorageMap::< + U256, + StorageMap>, + >::new(U256::from(500)); // Set nested values map.entry(U256::from(1)) @@ -240,7 +246,10 @@ mod tests { fn test_map_with_arrays_as_values() { let mut sdk = MockStorage::new(); // Map, 3>> - let map = StorageMap::, 3>>::new(U256::from(600)); + let map = + StorageMap::, 3>>::new( + U256::from(600), + ); // Set array values for key 1 let array1 = map.entry(U256::from(1)); @@ -267,7 +276,8 @@ mod tests { #[test] fn test_map_overwrites() { let mut sdk = MockStorage::new(); - let map = StorageMap::>::new(U256::from(700)); + let map = + StorageMap::>::new(U256::from(700)); // Set initial value map.entry(U256::from(42)).set(&mut sdk, U256::from(100)); @@ -283,8 +293,10 @@ mod tests { let mut sdk = MockStorage::new(); // Two maps at different slots - let map1 = StorageMap::>::new(U256::from(800)); - let map2 = StorageMap::>::new(U256::from(801)); + let map1 = + StorageMap::>::new(U256::from(800)); + let map2 = + StorageMap::>::new(U256::from(801)); // Same key, different values map1.entry(U256::from(1)).set(&mut sdk, U256::from(111)); @@ -298,7 +310,8 @@ mod tests { #[test] fn test_map_storage_layout() { let mut sdk = MockStorage::new(); - let map = StorageMap::>::new(U256::from(5)); + let map = + StorageMap::>::new(U256::from(5)); // Set value for key = 7 let key = U256::from(7); @@ -321,7 +334,8 @@ mod tests { #[test] fn test_map_with_packed_values() { let mut sdk = MockStorage::new(); - let map = StorageMap::>::new(U256::from(10)); + let map = + StorageMap::>::new(U256::from(10)); // Set u64 value for key = 1 map.entry(U256::from(1)) @@ -343,7 +357,8 @@ mod tests { let mut sdk = MockStorage::new(); // Test bool key storage - let bool_map = StorageMap::>::new(U256::from(20)); + let bool_map = + StorageMap::>::new(U256::from(20)); bool_map.entry(true).set(&mut sdk, U256::from(100)); // Calculate slot for true (encoded as 1) @@ -354,7 +369,8 @@ mod tests { assert_eq!(sdk.get_slot(slot_true), U256::from(100)); // Test string key storage - let string_map = StorageMap::<&str, StoragePrimitive>::new(U256::from(30)); + let string_map = + StorageMap::<&str, StoragePrimitive>::new(U256::from(30)); string_map.entry("test").set(&mut sdk, U256::from(999)); // Calculate slot for "test" @@ -368,7 +384,10 @@ mod tests { #[test] fn test_nested_maps_storage() { let mut sdk = MockStorage::new(); - let map = StorageMap::>>::new(U256::from(40)); + let map = StorageMap::< + U256, + StorageMap>, + >::new(U256::from(40)); // Set map[1][2] = 100 map.entry(U256::from(1)) @@ -394,7 +413,10 @@ mod tests { #[test] fn test_map_with_array_values_storage() { let mut sdk = MockStorage::new(); - let map = StorageMap::, 3>>::new(U256::from(50)); + let map = + StorageMap::, 3>>::new( + U256::from(50), + ); // Set array values for key = 5 let array = map.entry(U256::from(5)); @@ -421,8 +443,10 @@ mod tests { let mut sdk = MockStorage::new(); // Two maps at different slots - let map1 = StorageMap::>::new(U256::from(60)); - let map2 = StorageMap::>::new(U256::from(61)); + let map1 = + StorageMap::>::new(U256::from(60)); + let map2 = + StorageMap::>::new(U256::from(61)); // Same key, different values map1.entry(U256::from(1)).set(&mut sdk, U256::from(111)); @@ -448,7 +472,8 @@ mod tests { #[test] fn test_map_zero_key() { let mut sdk = MockStorage::new(); - let map = StorageMap::>::new(U256::from(70)); + let map = + StorageMap::>::new(U256::from(70)); // Test with key = 0 map.entry(U256::ZERO).set(&mut sdk, U256::from(0xABCDEF)); diff --git a/crates/sdk/src/storage/mod.rs b/crates/sdk/src/storage/mod.rs index 10d27a64e..e10d3b572 100644 --- a/crates/sdk/src/storage/mod.rs +++ b/crates/sdk/src/storage/mod.rs @@ -1,224 +1,152 @@ +//! Storage abstraction layer for Ethereum smart contracts. + use crate::{B256, U256}; -use alloc::vec::Vec; use fluentbase_types::StorageAPI; -pub mod primitive; - -pub mod array; -pub mod bytes; -pub mod map; - -pub mod composite; +mod array; +mod bytes; +mod map; +mod primitive; +mod vec; + +pub use array::*; +pub use bytes::*; +pub use map::*; +pub use primitive::*; +pub use vec::*; +// test utils pub mod mock; -pub use mock::MockStorage; - -pub mod vec; -/// The main trait that connects a Rust type to its storage layout and access API. -/// Any type that can be a field in a `#[fluent_storage]` struct must implement this. +/// Trait connecting Rust types to Ethereum's storage model. +/// +/// Ethereum storage is a key-value store with 2^256 slots, each holding 32 bytes. +/// This trait defines how Rust types map to these slots through two key concepts: +/// +/// 1. **Storage Layout** - How much space a type needs: +/// - Primitive types < 32 bytes can be packed together in one slot +/// - Full-width types (32 bytes) occupy exactly one slot +/// - Complex types (arrays, structs) may span multiple slots +/// +/// 2. **Two-phase Access Pattern**: +/// - **Descriptor**: Lightweight metadata about WHERE data lives (slot + offset) +/// - **Accessor**: The actual API for HOW to read/write that data +/// +/// This separation allows zero-cost abstractions: descriptors are computed at +/// compile-time, while accessors provide type-safe runtime operations. pub trait StorageLayout: Sized { - /// A lightweight, `Copy`-able struct describing the storage location. - type Descriptor: StorageDescriptor; + /// Lightweight location metadata. + /// + /// For packable types: contains slot + byte offset + /// For full types: contains only slot number + /// Computed at compile-time, Copy + Send + Sync + type Descriptor: StorageDescriptor + Copy; - /// A temporary proxy object providing the interactive API for this type. + /// Runtime access interface. + /// + /// Different types have different accessors: + /// - Primitive: simple get/set operations + /// - Array: indexed access via at(index) + /// - Map: key-based access via entry(key) + /// - Vec: dynamic operations (push/pop/at) type Accessor; - /// The number of contiguous slots required by this type's layout. - const REQUIRED_SLOTS: usize; + /// Size in bytes when encoded to storage. + /// + /// Used to determine: + /// - Whether type can be packed (BYTES < 32) + /// - Offset calculation for packed fields + /// - Total storage consumption + const BYTES: usize; - const ENCODED_SIZE: usize; + /// Number of storage slots this type reserves. + /// + /// - 0: Type can be packed with others (bool, u8, u16, etc.) + /// - 1: Type uses exactly one slot (u256, address for full slot) + /// - N: Type uses N slots (arrays, structs, etc.) + /// + /// Note: SLOTS = 0 means the type doesn't reserve a full slot, + /// allowing the compiler to pack multiple such types together. + const SLOTS: usize; - /// Creates an Accessor, the sole entry point for interacting with the stored value. + /// Construct accessor from descriptor. + /// + /// This is the entry point for all storage operations. + /// The descriptor tells WHERE, the accessor provides HOW. fn access(descriptor: Self::Descriptor) -> Self::Accessor; } -/// Trait that all storage descriptors must implement. -/// Provides a uniform interface for creating and accessing storage locations. -pub trait StorageDescriptor { - /// Creates a descriptor at the specified storage location. - /// For composite types (arrays, structs, maps), offset should be 0. - /// For packed primitive types, offset indicates position within the slot. +/// Uniform interface for creating storage descriptors at specific locations. +pub trait StorageDescriptor: Copy { fn new(slot: U256, offset: u8) -> Self; - - /// Returns the base storage slot. fn slot(&self) -> U256; - - /// Returns the offset within the slot (0 for non-packed types). fn offset(&self) -> u8; } -/// A trait for types that have a fixed-size byte representation suitable for storage. +/// Encoding/decoding for types that fit within a single storage slot. /// -/// This trait defines the low-level serialization and deserialization logic -/// for types that can be packed within a single 32-byte storage slot. -pub trait PrimitiveCodec: Sized { - /// The exact number of bytes this type occupies when encoded. Must be <= 32. +/// Implementors must guarantee: +/// - ENCODED_SIZE <= 32 +/// - encode_into/decode are inverse operations +/// - Big-endian encoding for consistency with EVM +pub trait PackableCodec: Sized + Copy { + /// Exact size in bytes (must be <= 32). const ENCODED_SIZE: usize; - /// Encodes the value into the provided byte slice. - /// - /// # Panics - /// if the length of `target` is not equal to `ENCODED_SIZE`. + /// Encode value to bytes at target position. fn encode_into(&self, target: &mut [u8]); - /// Decodes a value from the provided byte slice. - /// - /// # Panics - /// Panics if the length of `bytes` is not equal to `ENCODED_SIZE`. + /// Decode value from bytes. fn decode(bytes: &[u8]) -> Self; } -/// ---------------------------------- -/// Accessor traits -/// ---------------------------------- -/// Defines the API for a single, primitive value that implements `StorageCodec`. -pub trait PrimitiveAccess { - /// Reads the value from storage. - fn get(&self, sdk: &S) -> T; - - /// Writes a new value to storage. - fn set(&self, sdk: &mut S, value: T); -} - -/// Defines the API for a fixed-size array of `StorageLayout` elements. -pub trait ArrayAccess { - /// Returns an accessor for the element at the given index. - /// - /// # Panics - /// Panics if `index >= N`. - fn at(&self, index: usize) -> T::Accessor; -} -/// Defines the API for a key-value map where values are `StorageLayout` types. -pub trait MapAccess { - /// Returns an accessor for the value at the given key. - /// The accessor can be used to get or set the value. - fn entry(&self, key: K) -> V::Accessor; -} - -/// Defines the API for a dynamic vector of `StorageLayout` elements. -/// Dynamic vectors in Solidity store their length at the base slot, -/// and elements are stored starting at keccak256(base_slot). -pub trait VecAccess { - /// Returns the number of elements in the vector. - fn len(&self, sdk: &S) -> u64; - - /// Returns `true` if the vector is empty. - fn is_empty(&self, sdk: &S) -> bool { - self.len(sdk) == 0 - } - - /// Returns an accessor for the element at `index`. - /// # Panics - /// Panics if `index >= self.len()`. - fn at(&self, index: u64) -> T::Accessor; - - /// Appends a new element and returns an accessor to initialize it. - /// Updates the length and returns accessor to the new element. - fn push(&self, sdk: &mut S) -> T::Accessor; - - /// Removes the last element by decrementing the length. - /// Does not clear the storage slot (gas optimization). - fn pop(&self, sdk: &mut S); - - /// Clears the vector by setting length to 0. - /// Does not clear individual elements (gas optimization). - fn clear(&self, sdk: &mut S); -} -/// Defines the specialized API for dynamic byte arrays. -/// Dynamic byte arrays in Solidity use optimized storage: -/// - Short (< 32 bytes): data and length stored inline -/// - Long (≥ 32 bytes): length at base slot, data at keccak256(base_slot) -pub trait BytesAccess { - /// Returns the number of bytes. - fn len(&self, sdk: &S) -> usize; - - /// Returns `true` if the array is empty. - fn is_empty(&self, sdk: &S) -> bool { - self.len(sdk) == 0 - } - - /// Reads a single byte at `index`. - /// # Panics - /// Panics if `index >= self.len()`. - fn get(&self, sdk: &S, index: usize) -> u8; - - /// Appends a byte. - fn push(&self, sdk: &mut S, byte: u8); - - /// Removes and returns the last byte, or `None` if empty. - fn pop(&self, sdk: &mut S) -> Option; - - /// Reads all bytes into a `Vec`. - /// Use with caution due to gas costs for large arrays. - fn load(&self, sdk: &S) -> Vec; - - /// Overwrites the entire storage with new data. - /// Efficiently handles transition between short and long forms. - fn store(&self, sdk: &mut S, bytes: &[u8]); - - /// Clears all bytes by setting length to 0. - /// May clear storage slots for gas optimization. - fn clear(&self, sdk: &mut S); -} - -/// ---------------------------------- -/// Helper traits -/// ---------------------------------- -/// A trait for types that can be used as keys in a `Map`. -pub trait MapKey { - /// Computes the final storage slot for an item within a map. - fn compute_slot(&self, root_slot: U256) -> U256; -} - -/// An internal extension trait for `StorageAPI` providing low-level, -/// packed storage operations. Not intended for public use. +/// Low-level storage operations. +/// +/// Extension trait providing optimized read/write for packed values. +/// Automatically implemented for all types implementing StorageAPI. pub(crate) trait StorageOps: StorageAPI { - /// Reads a 32-byte word from storage, returning a B256. - /// Panics on syscall failure. + /// Read full 32-byte slot. fn sload(&self, slot: U256) -> B256 { self.storage(&slot).unwrap().into() } - /// Writes a 32-byte word to storage. - /// Panics on syscall failure. + /// Write full 32-byte slot. fn sstore(&mut self, slot: U256, value: B256) { self.write_storage(slot, value.into()).unwrap() } - /// Reads a value of type `T` that implements `StorageCodec` from a specific - /// location (slot + offset) within a storage word. - fn read_at(&self, slot: U256, offset: u8) -> T { - // offset is from the LEFT edge (start of byte array) + /// Read packed value from slot at specific offset. + /// + /// Optimization: single SLOAD even for small types. + fn read_at(&self, slot: U256, offset: u8) -> T { let start = offset as usize; let end = start + T::ENCODED_SIZE; - - assert!(end <= 32, "read out of slot bounds"); + assert!(end <= 32, "read out of bounds"); let word = self.sload(slot); T::decode(&word[start..end]) } - /// Writes a value of type `T` that implements `StorageCodec` to a specific - /// location (slot + offset) within a storage word. - fn write_at(&mut self, slot: U256, offset: u8, value: &T) { - // offset is from the LEFT edge (start of byte array) + /// Write packed value to slot at specific offset. + /// + /// Optimization: skip SLOAD for full-slot writes. + fn write_at(&mut self, slot: U256, offset: u8, value: &T) { let start = offset as usize; let end = start + T::ENCODED_SIZE; - - assert!(end <= 32, "write out of slot bounds"); + assert!(end <= 32, "write out of bounds"); if T::ENCODED_SIZE == 32 && offset == 0 { - let mut word_bytes = [0u8; 32]; - value.encode_into(&mut word_bytes); - self.sstore(slot, B256::from(word_bytes)); + // Full slot overwrite - no need to read existing value + let mut word = [0u8; 32]; + value.encode_into(&mut word); + self.sstore(slot, B256::from(word)); return; } + // Partial update - preserve other bytes in slot let mut word = self.sload(slot); value.encode_into(&mut word.0[start..end]); self.sstore(slot, word); } } -/// Automatically implement `StorageOps` for any type that already implements `StorageAPI`. impl StorageOps for S {} diff --git a/crates/sdk/src/storage/primitive.rs b/crates/sdk/src/storage/primitive.rs index 569bafb7a..4bf573203 100644 --- a/crates/sdk/src/storage/primitive.rs +++ b/crates/sdk/src/storage/primitive.rs @@ -1,34 +1,39 @@ -use crate::storage::{ - PrimitiveAccess, PrimitiveCodec, StorageDescriptor, StorageLayout, StorageOps, +use crate::{ + storage::{PackableCodec, StorageDescriptor, StorageLayout, StorageOps}, + Address, + FixedBytes, + Signed, + Uint, + U256, }; use core::marker::PhantomData; -use fluentbase_types::{Address, FixedBytes, Signed, StorageAPI, Uint, U256}; +use fluentbase_types::StorageAPI; -// --- 1. Descriptor --- - -/// A lightweight, copy-able descriptor that defines the storage location -/// of a single, packable value. -#[derive(Default, Copy, Clone, Debug, PartialEq, Eq)] +/// Storage descriptor and accessor for single packable values. +#[derive(Debug, PartialEq, Eq)] pub struct StoragePrimitive { slot: U256, offset: u8, _marker: PhantomData, } -impl StoragePrimitive { - /// Creates a new descriptor for a primitive value at a specific location. - pub const fn new(slot: U256, offset: u8) -> Self { - Self { - slot, - offset, - _marker: PhantomData, - } +// Manual Copy/Clone to avoid T: Copy bound. +// StoragePrimitive is just metadata (location), not the actual stored value. +impl Clone for StoragePrimitive { + fn clone(&self) -> Self { + *self } } +impl Copy for StoragePrimitive {} + impl StorageDescriptor for StoragePrimitive { fn new(slot: U256, offset: u8) -> Self { - Self::new(slot, offset) + Self { + slot, + offset, + _marker: PhantomData, + } } fn slot(&self) -> U256 { @@ -40,110 +45,82 @@ impl StorageDescriptor for StoragePrimitive { } } -// --- 2. Accessor --- - -/// A lightweight accessor that holds only the storage location descriptor. -/// SDK is passed to methods that need storage access. -pub struct PrimitiveAccessor { - descriptor: StoragePrimitive, -} - -impl PrimitiveAccessor { - /// Creates a new accessor. - pub(crate) fn new(descriptor: StoragePrimitive) -> Self { - Self { descriptor } - } -} - -// --- 3. API Implementation (impl PrimitiveAccess for PrimitiveAccessor) --- - -impl PrimitiveAccess for PrimitiveAccessor { - /// Reads the value from its storage slot, decodes it, and returns it. - fn get(&self, sdk: &S) -> T { - sdk.read_at(self.descriptor.slot, self.descriptor.offset) +impl StoragePrimitive { + /// Read value from storage. + pub fn get(&self, sdk: &S) -> T { + sdk.read_at(self.slot, self.offset) } - /// Encodes and writes a new value to its storage slot. - fn set(&self, sdk: &mut S, value: T) { - sdk.write_at(self.descriptor.slot, self.descriptor.offset, &value); + /// Write value to storage. + pub fn set(&self, sdk: &mut S, value: T) { + sdk.write_at(self.slot, self.offset, &value); } } -// --- 4. Main Trait Implementation (impl StorageLayout for Primitive) --- - -impl StorageLayout for StoragePrimitive { - /// The descriptor for a `Primitive` is the struct itself. +impl StorageLayout for StoragePrimitive { type Descriptor = Self; + type Accessor = Self; - /// The accessor for a `Primitive` is the `PrimitiveAccessor`. - type Accessor = PrimitiveAccessor; - - /// A single primitive value, even if small, reserves at least one full slot - /// in the storage layout calculation to avoid complex packing across fields. - /// Packing happens *within* the slot, managed by the `offset`. - const REQUIRED_SLOTS: usize = if T::ENCODED_SIZE == 32 { 1 } else { 0 }; - - const ENCODED_SIZE: usize = T::ENCODED_SIZE; + const BYTES: usize = T::ENCODED_SIZE; + const SLOTS: usize = if T::ENCODED_SIZE == 32 { 1 } else { 0 }; - /// The entry point to interacting with the primitive value. fn access(descriptor: Self::Descriptor) -> Self::Accessor { - PrimitiveAccessor::new(descriptor) + descriptor } } -// --- 5. StorageCodec Implementations for Core Primitives --- +// --- PackableCodec implementations --- -impl PrimitiveCodec for Address { - const ENCODED_SIZE: usize = 20; +impl PackableCodec for bool { + const ENCODED_SIZE: usize = 1; fn encode_into(&self, target: &mut [u8]) { - target.copy_from_slice(self.as_slice()); + target[0] = *self as u8; } fn decode(bytes: &[u8]) -> Self { - Address::from_slice(bytes) + bytes[0] != 0 } } -impl PrimitiveCodec for bool { - const ENCODED_SIZE: usize = 1; +impl PackableCodec for Address { + const ENCODED_SIZE: usize = 20; fn encode_into(&self, target: &mut [u8]) { - target[0] = *self as u8; + target.copy_from_slice(self.as_slice()); } fn decode(bytes: &[u8]) -> Self { - bytes[0] != 0 + Address::from_slice(bytes) } } -// Macro for alloy_primitives::Uint types -macro_rules! impl_uint_codec { - ($($bits:literal => $limbs:literal),* $(,)?) => { +// Standard Rust integers +macro_rules! impl_int_codec { + ($($ty:ty),*) => { $( - impl PrimitiveCodec for Uint<$bits, $limbs> { - const ENCODED_SIZE: usize = $bits / 8; + impl PackableCodec for $ty { + const ENCODED_SIZE: usize = core::mem::size_of::<$ty>(); fn encode_into(&self, target: &mut [u8]) { - debug_assert_eq!(target.len(), Self::ENCODED_SIZE); - let bytes = self.to_be_bytes::<{ $bits / 8 }>(); - target.copy_from_slice(&bytes); + target.copy_from_slice(&self.to_be_bytes()); } fn decode(bytes: &[u8]) -> Self { - debug_assert_eq!(bytes.len(), Self::ENCODED_SIZE); - Self::from_be_bytes::<{ $bits / 8 }>(bytes.try_into().unwrap()) + <$ty>::from_be_bytes(bytes.try_into().unwrap()) } } )* }; } -// Macro for alloy_primitives::Signed types -macro_rules! impl_signed_codec { - ($($bits:literal => $limbs:literal),* $(,)?) => { +impl_int_codec!(u8, u16, u32, u64, u128, i8, i16, i32, i64, i128); + +// Macro for Uint types (standard Solidity sizes) +macro_rules! impl_uint_codec { + ($($bits:literal => $limbs:literal),*) => { $( - impl PrimitiveCodec for Signed<$bits, $limbs> { + impl PackableCodec for Uint<$bits, $limbs> { const ENCODED_SIZE: usize = $bits / 8; fn encode_into(&self, target: &mut [u8]) { @@ -161,54 +138,34 @@ macro_rules! impl_signed_codec { }; } -// Macro for Rust unsigned integer types (excluding those already implemented) -macro_rules! impl_rust_uint_codec { - ($($ty:ty),* $(,)?) => { - $( - impl PrimitiveCodec for $ty { - const ENCODED_SIZE: usize = core::mem::size_of::<$ty>(); - - fn encode_into(&self, target: &mut [u8]) { - debug_assert_eq!(target.len(), Self::ENCODED_SIZE); - target.copy_from_slice(&self.to_be_bytes()); - } - - fn decode(bytes: &[u8]) -> Self { - debug_assert_eq!(bytes.len(), Self::ENCODED_SIZE); - <$ty>::from_be_bytes(bytes.try_into().unwrap()) - } - } - )* - }; -} - -// Macro for Rust signed integer types -macro_rules! impl_rust_sint_codec { - ($($ty:ty),* $(,)?) => { +// Macro for Signed types +macro_rules! impl_signed_codec { + ($($bits:literal => $limbs:literal),*) => { $( - impl PrimitiveCodec for $ty { - const ENCODED_SIZE: usize = core::mem::size_of::<$ty>(); + impl PackableCodec for Signed<$bits, $limbs> { + const ENCODED_SIZE: usize = $bits / 8; fn encode_into(&self, target: &mut [u8]) { debug_assert_eq!(target.len(), Self::ENCODED_SIZE); - target.copy_from_slice(&self.to_be_bytes()); + let bytes = self.to_be_bytes::<{ $bits / 8 }>(); + target.copy_from_slice(&bytes); } fn decode(bytes: &[u8]) -> Self { debug_assert_eq!(bytes.len(), Self::ENCODED_SIZE); - <$ty>::from_be_bytes(bytes.try_into().unwrap()) + Self::from_be_bytes::<{ $bits / 8 }>(bytes.try_into().unwrap()) } } )* }; } -// Macro for alloy_primitives::FixedBytes types +// Macro for FixedBytes types macro_rules! impl_fixed_bytes_codec { - ($($bytes:literal),* $(,)?) => { + ($($n:literal),*) => { $( - impl PrimitiveCodec for FixedBytes<$bytes> { - const ENCODED_SIZE: usize = $bytes; + impl PackableCodec for FixedBytes<$n> { + const ENCODED_SIZE: usize = $n; fn encode_into(&self, target: &mut [u8]) { debug_assert_eq!(target.len(), Self::ENCODED_SIZE); @@ -224,7 +181,7 @@ macro_rules! impl_fixed_bytes_codec { }; } -// Apply macros for all Uint types (LIMBS = (BITS + 63) / 64) +// Standard Solidity uint types (LIMBS = (BITS + 63) / 64) impl_uint_codec! { 8 => 1, 16 => 1, @@ -257,10 +214,10 @@ impl_uint_codec! { 232 => 4, 240 => 4, 248 => 4, - 256 => 4, + 256 => 4 } -// Apply macros for all Signed types +// Standard Solidity int types impl_signed_codec! { 8 => 1, 16 => 1, @@ -293,336 +250,142 @@ impl_signed_codec! { 232 => 4, 240 => 4, 248 => 4, - 256 => 4, -} - -// Apply macros for Rust primitive types -impl_rust_uint_codec! { - u8, u16, u32, u64, u128, usize -} - -impl_rust_sint_codec! { - i8, i16, i32, i64, i128, isize + 256 => 4 } +// Standard Solidity bytes types impl_fixed_bytes_codec! { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, - 25, 26, 27, 28, 29, 30, 31, 32, + 25, 26, 27, 28, 29, 30, 31, 32 +} + +macro_rules! storage_alias { + ($($name:ident => $type:ty),* $(,)?) => { + $( + pub type $name = StoragePrimitive<$type>; + )* + }; } +storage_alias! { + // Essential types + StorageBool => bool, + StorageAddress => Address, + StorageU256 => U256, + + // Common Rust integers + StorageU8 => u8, + StorageU16 => u16, + StorageU32 => u32, + StorageU64 => u64, + StorageU128 => u128, + StorageI8 => i8, + StorageI16 => i16, + StorageI32 => i32, + StorageI64 => i64, + StorageI128 => i128, + + // Most used Solidity-compatible types + StorageUint8 => Uint<8, 1>, + StorageUint16 => Uint<16, 1>, + StorageUint32 => Uint<32, 1>, + StorageUint64 => Uint<64, 1>, + StorageUint128 => Uint<128, 2>, + StorageUint256 => Uint<256, 4>, + + StorageInt8 => Signed<8, 1>, + StorageInt16 => Signed<16, 1>, + StorageInt32 => Signed<32, 1>, + StorageInt64 => Signed<64, 1>, + StorageInt128 => Signed<128, 2>, + StorageInt256 => Signed<256, 4>, + + StorageBytes4 => FixedBytes<4>, + StorageBytes8 => FixedBytes<8>, + StorageBytes16 => FixedBytes<16>, + StorageBytes20 => FixedBytes<20>, + StorageBytes32 => FixedBytes<32>, + StorageB256 => StoragePrimitive>, +} #[cfg(test)] mod tests { use super::*; use crate::storage::mock::MockStorage; #[test] - fn read_write_u256() { + fn test_packing_isolation() { + // Critical: verify packed values don't overwrite each other let mut sdk = MockStorage::new(); - let counter = StoragePrimitive::::new(U256::from(1), 0); - let accessor = as StorageLayout>::access(counter); - - let value = accessor.get(&sdk); - assert_eq!(value, U256::ZERO); - - accessor.set(&mut sdk, U256::from(1)); - let value = accessor.get(&sdk); - assert_eq!(value, U256::from(1)); + let slot = U256::from(0); + + // Pack: bool | u8 | u16 | u32 + // Offsets: 31 | 30 | 28 | 24 + let bool_val = StorageBool::new(slot, 31); + let u8_val = StorageU8::new(slot, 30); + let u16_val = StorageU16::new(slot, 28); + let u32_val = StorageU32::new(slot, 24); + + // Set all values + bool_val.set(&mut sdk, true); + u8_val.set(&mut sdk, 0xFF); + u16_val.set(&mut sdk, 0xABCD); + u32_val.set(&mut sdk, 0x12345678); + + // Verify isolation - each value preserved + assert!(bool_val.get(&sdk)); + assert_eq!(u8_val.get(&sdk), 0xFF); + assert_eq!(u16_val.get(&sdk), 0xABCD); + assert_eq!(u32_val.get(&sdk), 0x12345678); + + // Overwrite one value and check others unchanged + u16_val.set(&mut sdk, 0x5678); + assert!(bool_val.get(&sdk)); + assert_eq!(u8_val.get(&sdk), 0xFF); + assert_eq!(u16_val.get(&sdk), 0x5678); + assert_eq!(u32_val.get(&sdk), 0x12345678); } #[test] - fn set_and_get_work_correctly() { + fn test_full_slot_optimization() { + // Verify that full slot writes don't do unnecessary reads let mut sdk = MockStorage::new(); - let counter = StoragePrimitive::::new(U256::from(2), 0); - let new_value = U256::from(12345); - let accessor = as StorageLayout>::access(counter); - - // Act: Set the value - accessor.set(&mut sdk, new_value); - - // Assert: Read it back - let read_value = accessor.get(&sdk); - assert_eq!(read_value, new_value); - - // Assert: Check raw slot content - assert_eq!(sdk.get_slot(U256::from(2)), new_value); - } - - #[test] - fn packed_values_do_not_interfere() { - let mut sdk = MockStorage::new(); - let slot = U256::from(3); - - // Layout: [ u64 (offset 23) | bool (offset 31) ] - let flag_accessor = - as StorageLayout>::access(StoragePrimitive::::new( - slot, 31, - )); - let timestamp_accessor = as StorageLayout>::access( - StoragePrimitive::::new(slot, 23), - ); - - // Act - flag_accessor.set(&mut sdk, true); - timestamp_accessor.set(&mut sdk, 0xDEADBEEFCAFEBABE); - // Assert - assert!(flag_accessor.get(&sdk)); - assert_eq!(timestamp_accessor.get(&sdk), 0xDEADBEEFCAFEBABE); - - // Assert raw slot content via snapshot - let expected_hex = "0000000000000000000000000000000000000000000000deadbeefcafebabe01"; - assert_eq!(sdk.get_slot_hex(U256::from(3)), expected_hex); - } - - #[test] - fn set_optimizes_for_full_width_types() { - let mut sdk = MockStorage::new(); - // Initialize slot with junk data + // Pre-fill slot with garbage sdk.init_slot( - U256::from(3), + U256::from(1), "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", ); - let accessor = as StorageLayout>::access( - StoragePrimitive::::new(U256::from(4), 0), - ); - let new_value = U256::from(1); - - // Act: This `set` should NOT read the old value but overwrite it completely. - accessor.set(&mut sdk, new_value); + // Write full slot value - should overwrite without reading + let value = StorageU256::new(U256::from(1), 0); + value.set(&mut sdk, U256::from(42)); - // Assert + // Verify complete overwrite assert_eq!( - sdk.get_slot_hex(U256::from(4)), - "0000000000000000000000000000000000000000000000000000000000000001" - ); - } - - #[test] - fn different_primitive_types() { - let mut sdk = MockStorage::new(); - let slot = U256::from(5); - - // Test different primitive types - let bool_accessor = - as StorageLayout>::access(StoragePrimitive::::new( - slot, 31, - )); - let u8_accessor = - as StorageLayout>::access(StoragePrimitive::::new(slot, 30)); - let u16_accessor = as StorageLayout>::access( - StoragePrimitive::::new(slot, 28), + sdk.get_slot_hex(U256::from(1)), + "000000000000000000000000000000000000000000000000000000000000002a" ); - let u32_accessor = as StorageLayout>::access( - StoragePrimitive::::new(slot, 24), - ); - - // Set values - bool_accessor.set(&mut sdk, true); - u8_accessor.set(&mut sdk, 0xFF); - u16_accessor.set(&mut sdk, 0xABCD); - u32_accessor.set(&mut sdk, 0x12345678); - - // Verify values - assert!(bool_accessor.get(&sdk)); - assert_eq!(u8_accessor.get(&sdk), 0xFF); - assert_eq!(u16_accessor.get(&sdk), 0xABCD); - assert_eq!(u32_accessor.get(&sdk), 0x12345678); } #[test] - fn test_uint_types() { + fn test_signed_encoding() { + // Critical: verify two's complement encoding for negative values let mut sdk = MockStorage::new(); - // Test U8 (Uint<8, 1>) - let u8_accessor = > as StorageLayout>::access( - StoragePrimitive::>::new(U256::from(10), 31), - ); - u8_accessor.set(&mut sdk, Uint::<8, 1>::from(255)); - assert_eq!(u8_accessor.get(&sdk), Uint::<8, 1>::from(255)); - - // Test U128 (Uint<128, 2>) - let u128_accessor = - > as StorageLayout>::access(StoragePrimitive::< - Uint<128, 2>, - >::new( - U256::from(11), 16 - )); - let u128_val = Uint::<128, 2>::from(0x12345678_9ABCDEF0_u128); - u128_accessor.set(&mut sdk, u128_val); - assert_eq!(u128_accessor.get(&sdk), u128_val); - } + let i8_val = StoragePrimitive::::new(U256::from(2), 31); + let i128_val = StoragePrimitive::::new(U256::from(3), 16); - #[test] - fn test_signed_types() { - let mut sdk = MockStorage::new(); + // Test negative values + i8_val.set(&mut sdk, -1); + i128_val.set(&mut sdk, -42); - // Test I8 (Signed<8, 1>) - let i8_accessor = - > as StorageLayout>::access(StoragePrimitive::< - Signed<8, 1>, - >::new( - U256::from(20), 31 - )); - i8_accessor.set(&mut sdk, Signed::<8, 1>::try_from(-42).unwrap()); - assert_eq!( - i8_accessor.get(&sdk), - Signed::<8, 1>::try_from(-42).unwrap() - ); - - // Test I256 with negative value - let i256_accessor = - > as StorageLayout>::access(StoragePrimitive::< - Signed<256, 4>, - >::new( - U256::from(21), 0 - )); - let negative_val = Signed::<256, 4>::try_from(-1000000).unwrap(); - i256_accessor.set(&mut sdk, negative_val); - assert_eq!(i256_accessor.get(&sdk), negative_val); - } - - #[test] - fn test_fixed_bytes_types() { - let mut sdk = MockStorage::new(); - - // Test FixedBytes<1> (bytes1) - let bytes1_accessor = - > as StorageLayout>::access(StoragePrimitive::< - FixedBytes<1>, - >::new( - U256::from(30), 31 - )); - let bytes1_val = FixedBytes::<1>::from([0xAB]); - bytes1_accessor.set(&mut sdk, bytes1_val); - assert_eq!(bytes1_accessor.get(&sdk), bytes1_val); - - // Test FixedBytes<20> (bytes20 - address size) - let bytes20_accessor = - > as StorageLayout>::access(StoragePrimitive::< - FixedBytes<20>, - >::new( - U256::from(31), 12 - )); - let bytes20_val = FixedBytes::<20>::from([0x42; 20]); - bytes20_accessor.set(&mut sdk, bytes20_val); - assert_eq!(bytes20_accessor.get(&sdk), bytes20_val); - - // Test FixedBytes<32> (bytes32 - full slot) - let bytes32_accessor = - > as StorageLayout>::access(StoragePrimitive::< - FixedBytes<32>, - >::new( - U256::from(32), 0 - )); - let bytes32_val = FixedBytes::<32>::from([0xFF; 32]); - bytes32_accessor.set(&mut sdk, bytes32_val); - assert_eq!(bytes32_accessor.get(&sdk), bytes32_val); - } - - #[test] - fn test_rust_primitive_signed() { - let mut sdk = MockStorage::new(); - - // Test i8 - let i8_accessor = as StorageLayout>::access( - StoragePrimitive::::new(U256::from(40), 31), - ); - i8_accessor.set(&mut sdk, -128i8); - assert_eq!(i8_accessor.get(&sdk), -128i8); - - // Test i64 - let i64_accessor = as StorageLayout>::access( - StoragePrimitive::::new(U256::from(41), 24), - ); - i64_accessor.set(&mut sdk, -9223372036854775808i64); // i64::MIN - assert_eq!(i64_accessor.get(&sdk), -9223372036854775808i64); - } - - #[test] - fn test_rust_primitive_unsigned() { - let mut sdk = MockStorage::new(); - - // Test u128 - let u128_accessor = as StorageLayout>::access(StoragePrimitive::< - u128, - >::new( - U256::from(50), 16 - )); - u128_accessor.set(&mut sdk, 340282366920938463463374607431768211455u128); // u128::MAX - assert_eq!( - u128_accessor.get(&sdk), - 340282366920938463463374607431768211455u128 - ); - } - - #[test] - fn test_mixed_packing() { - let mut sdk = MockStorage::new(); - let slot = U256::from(60); - - // Pack: I8 | U16 | FixedBytes<4> | bool - // Offsets: 31 | 29 | 25 | 24 - - let i8_accessor = - > as StorageLayout>::access(StoragePrimitive::< - Signed<8, 1>, - >::new(slot, 31)); - let u16_accessor = > as StorageLayout>::access( - StoragePrimitive::>::new(slot, 29), - ); - let bytes4_accessor = - > as StorageLayout>::access(StoragePrimitive::< - FixedBytes<4>, - >::new(slot, 25)); - let bool_accessor = - as StorageLayout>::access(StoragePrimitive::::new( - slot, 24, - )); - - // Set values - i8_accessor.set(&mut sdk, Signed::<8, 1>::try_from(-1).unwrap()); - u16_accessor.set(&mut sdk, Uint::<16, 1>::from(0xBEEF)); - bytes4_accessor.set(&mut sdk, FixedBytes::<4>::from([0xDE, 0xAD, 0xBE, 0xEF])); - bool_accessor.set(&mut sdk, true); - - // Verify all values remain correct - assert_eq!(i8_accessor.get(&sdk), Signed::<8, 1>::try_from(-1).unwrap()); - assert_eq!(u16_accessor.get(&sdk), Uint::<16, 1>::from(0xBEEF)); - assert_eq!( - bytes4_accessor.get(&sdk), - FixedBytes::<4>::from([0xDE, 0xAD, 0xBE, 0xEF]) - ); - assert!(bool_accessor.get(&sdk)); - } - - #[test] - fn test_edge_cases() { - let mut sdk = MockStorage::new(); + assert_eq!(i8_val.get(&sdk), -1); + assert_eq!(i128_val.get(&sdk), -42); - // Test signed MIN and MAX values - let i128_accessor = - > as StorageLayout>::access(StoragePrimitive::< - Signed<128, 2>, - >::new( - U256::from(70), 16 - )); - - // Test MIN - let min_val = Signed::<128, 2>::MIN; - i128_accessor.set(&mut sdk, min_val); - assert_eq!(i128_accessor.get(&sdk), min_val); - - // Test MAX - let max_val = Signed::<128, 2>::MAX; - i128_accessor.set(&mut sdk, max_val); - assert_eq!(i128_accessor.get(&sdk), max_val); - - // Test -1 (all bits set) - let neg_one = Signed::<128, 2>::try_from(-1).unwrap(); - i128_accessor.set(&mut sdk, neg_one); - assert_eq!(i128_accessor.get(&sdk), neg_one); + // Verify encoding in storage (two's complement) + let slot2 = sdk.get_slot_hex(U256::from(2)); + assert_eq!(&slot2[62..64], "ff"); // -1 as i8 = 0xFF } } diff --git a/crates/sdk/src/storage/vec.rs b/crates/sdk/src/storage/vec.rs index ba171593d..43dab3f63 100644 --- a/crates/sdk/src/storage/vec.rs +++ b/crates/sdk/src/storage/vec.rs @@ -1,25 +1,36 @@ use crate::{ keccak256, - storage::{StorageDescriptor, StorageLayout, StorageOps, VecAccess}, - B256, U256, + storage::{ + primitive::StoragePrimitive, + PackableCodec, + StorageDescriptor, + StorageLayout, + StorageOps, + }, + B256, + U256, }; use core::marker::PhantomData; use fluentbase_types::StorageAPI; -// --- 1. Vec Descriptor --- - -/// A descriptor for a dynamic vector in storage. -/// Follows Solidity's dynamic array storage layout: -/// - Length stored at base slot -/// - Elements start at keccak256(base_slot) -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Dynamic vector in storage. +/// Length at base slot, elements at keccak256(base_slot). +#[derive(Debug, PartialEq, Eq)] pub struct StorageVec { base_slot: U256, _marker: PhantomData, } +// Manual Copy/Clone to avoid T: Copy bound +impl Clone for StorageVec { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for StorageVec {} + impl StorageVec { - /// Creates a new vector descriptor at the given base slot. pub const fn new(base_slot: U256) -> Self { Self { base_slot, @@ -27,58 +38,18 @@ impl StorageVec { } } - /// Computes the storage slot for vector elements. - /// Elements are stored starting at keccak256(base_slot). + /// Storage slot where elements start (keccak256 of base slot). fn elements_base_slot(&self) -> U256 { - // In Solidity, dynamic array elements start at keccak256(p) - // where p is the base slot padded to 32 bytes let hash = keccak256(self.base_slot.to_be_bytes::<32>()); U256::from_be_bytes(hash.0) } - - /// Calculates the slot and offset for an element at the given index. - fn element_location(&self, index: u64) -> (U256, u8) - where - T: StorageLayout, - { - let elements_base = self.elements_base_slot(); - - if T::REQUIRED_SLOTS == 0 { - // Primitive types that can potentially be packed - if T::ENCODED_SIZE < 32 { - // Packable primitive - multiple elements per slot - let elements_per_slot = 32 / T::ENCODED_SIZE; - let slot_index = index / elements_per_slot as u64; - let position_in_slot = index % elements_per_slot as u64; - - // Pack from left to right for arrays (high bytes to low bytes) - // First element at offset 0, second at offset T::ENCODED_SIZE, etc. - let offset = (32 - (position_in_slot + 1) * T::ENCODED_SIZE as u64) as u8; - let slot = elements_base + U256::from(slot_index); - - (slot, offset) - } else { - // Full-width primitive (32 bytes) - one per slot - let slot = elements_base + U256::from(index); - (slot, 0) - } - } else { - // Complex types - use REQUIRED_SLOTS slots per element - let slot = elements_base + U256::from(index * T::REQUIRED_SLOTS as u64); - (slot, 0) - } - } } impl StorageDescriptor for StorageVec { fn new(slot: U256, offset: u8) -> Self { debug_assert_eq!(offset, 0, "vectors always start at slot boundary"); - Self { - base_slot: slot, - _marker: PhantomData, - } + Self::new(slot) } - fn slot(&self) -> U256 { self.base_slot } @@ -88,84 +59,109 @@ impl StorageDescriptor for StorageVec { } } -// --- 2. VecAccess Implementation --- - -impl VecAccess for StorageVec +impl StorageVec where T::Descriptor: StorageDescriptor, { - fn len(&self, sdk: &S) -> u64 { - // Length is stored at the base slot - let length_word = sdk.sload(self.base_slot); - // Convert from B256 to u64 (taking the least significant 8 bytes) - let bytes = length_word.as_slice(); + /// Get current length of vector. + pub fn len(&self, sdk: &S) -> u64 { + let word = sdk.sload(self.base_slot); let mut len_bytes = [0u8; 8]; - len_bytes.copy_from_slice(&bytes[24..32]); + len_bytes.copy_from_slice(&word.0[24..32]); u64::from_be_bytes(len_bytes) } - fn is_empty(&self, sdk: &S) -> bool { + /// Check if vector is empty. + pub fn is_empty(&self, sdk: &S) -> bool { self.len(sdk) == 0 } - fn at(&self, index: u64) -> T::Accessor { + /// Calculate storage location for element at index. + fn element_location(&self, index: u64) -> (U256, u8) { + let elements_base = self.elements_base_slot(); + + if T::SLOTS == 0 { + // Packable elements + let elements_per_slot = 32 / T::BYTES; + let slot_index = index / elements_per_slot as u64; + let position_in_slot = index % elements_per_slot as u64; + + // Pack from right to left (Solidity convention) + let offset = (32 - (position_in_slot + 1) * T::BYTES as u64) as u8; + + (elements_base + U256::from(slot_index), offset) + } else { + // Non-packable elements + (elements_base + U256::from(index * T::SLOTS as u64), 0) + } + } + + /// Access element at index (no bounds check). + pub fn at(&self, index: u64) -> T::Accessor { let (slot, offset) = self.element_location(index); - let element_descriptor = T::Descriptor::new(slot, offset); - T::access(element_descriptor) + T::access(T::Descriptor::new(slot, offset)) } - fn push(&self, sdk: &mut S) -> T::Accessor { - // Get current length + /// Grow vector by one and return accessor to new element. + pub fn grow(&self, sdk: &mut S) -> T::Accessor { let current_len = self.len(sdk); - // Update length (increment by 1) + // Update length let new_len = current_len + 1; let mut len_bytes = [0u8; 32]; len_bytes[24..32].copy_from_slice(&new_len.to_be_bytes()); sdk.sstore(self.base_slot, B256::from(len_bytes)); - // Return accessor to the new element (at index = old length) - let (slot, offset) = self.element_location(current_len); - let element_descriptor = T::Descriptor::new(slot, offset); - T::access(element_descriptor) + // Return accessor to new element + self.at(current_len) } - fn pop(&self, sdk: &mut S) { + /// Shrink vector by one and return accessor to the removed element. + /// The accessor remains valid until the slot is reused. + pub fn shrink(&self, sdk: &mut S) -> Option { let current_len = self.len(sdk); if current_len == 0 { - return; // Nothing to pop + return None; } - // Update length (decrement by 1) - let new_len = current_len - 1; + let index = current_len - 1; + + // Update length first let mut len_bytes = [0u8; 32]; - len_bytes[24..32].copy_from_slice(&new_len.to_be_bytes()); + len_bytes[24..32].copy_from_slice(&index.to_be_bytes()); sdk.sstore(self.base_slot, B256::from(len_bytes)); - // Note: We don't clear the storage slot for gas optimization - // This matches Solidity's behavior + // Return accessor to removed element (still in storage) + Some(self.at(index)) } - - fn clear(&self, sdk: &mut S) { - // Set the length to 0 + /// Clear vector (sets length to 0). + pub fn clear(&self, sdk: &mut S) { sdk.sstore(self.base_slot, B256::ZERO); } } -// --- 3. StorageLayout Implementation --- +/// Specialized API for vectors of primitive types. +impl StorageVec> { + /// Push primitive value directly. + pub fn push(&self, sdk: &mut S, value: T) { + self.grow(sdk).set(sdk, value); + } + + /// Remove and return last value. + pub fn pop(&self, sdk: &mut S) -> Option { + self.shrink(sdk).map(|accessor| accessor.get(sdk)) + } +} impl StorageLayout for StorageVec where T::Descriptor: StorageDescriptor, { - type Descriptor = StorageVec; + type Descriptor = Self; type Accessor = Self; - // Dynamic vectors only need 1 slot for the length - // Elements are stored separately at keccak256(base_slot) - const REQUIRED_SLOTS: usize = 1; - - const ENCODED_SIZE: usize = 32; // Only the length is stored inline + const BYTES: usize = 32; // Only length stored inline + const SLOTS: usize = 1; // One slot for length fn access(descriptor: Self::Descriptor) -> Self::Accessor { descriptor @@ -175,165 +171,89 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{ - keccak256, - storage::{mock::MockStorage, primitive::StoragePrimitive, PrimitiveAccess}, + use crate::storage::{ + mock::MockStorage, + primitive::{StorageU256, StorageU64}, }; #[test] - fn test_vec_push_and_access() { + fn test_vec_primitive_api() { + // Critical: test specialized push/pop for primitives let mut sdk = MockStorage::new(); - let vec = StorageVec::>::new(U256::from(100)); + let vec = StorageVec::::new(U256::from(100)); - // Initially empty - assert_eq!(vec.len(&sdk), 0); - assert!(vec.is_empty(&sdk)); + // Push values + vec.push(&mut sdk, U256::from(111)); + vec.push(&mut sdk, U256::from(222)); + vec.push(&mut sdk, U256::from(333)); - // Push the first element - vec.push(&mut sdk).set(&mut sdk, U256::from(111)); - assert_eq!(vec.len(&sdk), 1); - - // Push the second element - vec.push(&mut sdk).set(&mut sdk, U256::from(222)); - assert_eq!(vec.len(&sdk), 2); + assert_eq!(vec.len(&sdk), 3); - // Access elements - need to check bounds first - let current_len = vec.len(&sdk); - assert!( - 0 < current_len, - "vector index out of bounds: index 0 >= length {current_len}" - ); - assert!( - 1 < current_len, - "vector index out of bounds: index 1 >= length {current_len}" - ); + // Pop values + assert_eq!(vec.pop(&mut sdk), Some(U256::from(333))); + assert_eq!(vec.pop(&mut sdk), Some(U256::from(222))); + assert_eq!(vec.len(&sdk), 1); + // Access remaining assert_eq!(vec.at(0).get(&sdk), U256::from(111)); - assert_eq!(vec.at(1).get(&sdk), U256::from(222)); - } - - #[test] - fn test_vec_pop() { - let mut sdk = MockStorage::new(); - let vec = StorageVec::>::new(U256::from(200)); - - // Push elements - vec.push(&mut sdk).set(&mut sdk, U256::from(100)); - vec.push(&mut sdk).set(&mut sdk, U256::from(200)); - vec.push(&mut sdk).set(&mut sdk, U256::from(300)); - assert_eq!(vec.len(&sdk), 3); - - // Pop one element - vec.pop(&mut sdk); - assert_eq!(vec.len(&sdk), 2); - // Remaining elements still accessible - assert_eq!(vec.at(0).get(&sdk), U256::from(100)); - assert_eq!(vec.at(1).get(&sdk), U256::from(200)); + // Pop from single element + assert_eq!(vec.pop(&mut sdk), Some(U256::from(111))); + assert_eq!(vec.pop(&mut sdk), None); // Empty } #[test] - fn test_vec_packed_elements() { + fn test_vec_packing() { + // Critical: verify elements pack correctly (right to left) let mut sdk = MockStorage::new(); - let vec = StorageVec::>::new(U256::from(300)); + let vec = StorageVec::::new(U256::from(200)); - // Push multiple u64 values (should pack) - vec.push(&mut sdk).set(&mut sdk, 0x1111111111111111u64); - vec.push(&mut sdk).set(&mut sdk, 0x2222222222222222u64); - vec.push(&mut sdk).set(&mut sdk, 0x3333333333333333u64); - vec.push(&mut sdk).set(&mut sdk, 0x4444444444444444u64); + // Push 5 u64 values - should use 2 slots + vec.push(&mut sdk, 0x1111111111111111u64); + vec.push(&mut sdk, 0x2222222222222222u64); + vec.push(&mut sdk, 0x3333333333333333u64); + vec.push(&mut sdk, 0x4444444444444444u64); + vec.push(&mut sdk, 0x5555555555555555u64); - assert_eq!(vec.len(&sdk), 4); - - // Verify packing by checking the storage - // Elements should be packed in the first slot after keccak(base_slot) let elements_base = { - let hash = keccak256(U256::from(300).to_be_bytes::<32>()); + let hash = keccak256(U256::from(200).to_be_bytes::<32>()); U256::from_be_bytes(hash.0) }; - // All 4 u64 values should be packed in one slot - let slot_content = sdk.get_slot_hex(elements_base); + // First 4 packed in slot 0 assert_eq!( - slot_content, + sdk.get_slot_hex(elements_base), "4444444444444444333333333333333322222222222222221111111111111111" ); - } - #[test] - fn test_vec_packed_elements2() { - let mut sdk = MockStorage::new(); - let vec = StorageVec::>::new(U256::from(300)); - - vec.push(&mut sdk).set(&mut sdk, 0x1111111111111111u64); - vec.push(&mut sdk).set(&mut sdk, 0x2222222222222222u64); - vec.push(&mut sdk).set(&mut sdk, 0x3333333333333333u64); - vec.push(&mut sdk).set(&mut sdk, 0x4444444444444444u64); - - let elements_base = { - let hash = keccak256(U256::from(300).to_be_bytes::<32>()); - U256::from_be_bytes(hash.0) - }; - - // Добавьте отладочный вывод - println!("Elements base slot: {elements_base:?}"); - let raw_value = sdk.get_slot(elements_base); - println!("Raw U256 value: {raw_value:?}"); - println!("As hex: {}", sdk.get_slot_hex(elements_base)); - - // Проверим offset для каждого элемента - for i in 0..4 { - let (slot, offset) = vec.element_location(i); - println!("Element {i}: slot={slot:?}, offset={offset}"); - } - } - - #[test] - fn test_vec_clear() { - let mut sdk = MockStorage::new(); - let vec = StorageVec::>::new(U256::from(400)); - - // Push elements - vec.push(&mut sdk).set(&mut sdk, U256::from(1)); - vec.push(&mut sdk).set(&mut sdk, U256::from(2)); - assert_eq!(vec.len(&sdk), 2); - - // Clear - vec.clear(&mut sdk); - assert_eq!(vec.len(&sdk), 0); - assert!(vec.is_empty(&sdk)); - } - - #[test] - fn test_vec_bounds_check() { - let mut sdk = MockStorage::new(); - let vec = StorageVec::>::new(U256::from(500)); - - vec.push(&mut sdk).set(&mut sdk, U256::from(100)); - - // Test that we can access a valid index - assert_eq!(vec.at(0).get(&sdk), U256::from(100)); - - // Note: at() method itself doesn't perform bound's checking since it doesn't have access to SDK - // Bounds checking would need to be done by the caller or in the accessor methods + // Fifth in slot 1 + assert_eq!( + sdk.get_slot_hex(elements_base + U256::from(1)), + "0000000000000000000000000000000000000000000000005555555555555555" + ); } #[test] - fn test_nested_vec() { + fn test_vec_complex_types() { + // Critical: verify grow/shrink for non-primitive types let mut sdk = MockStorage::new(); - let vec = StorageVec::>>::new(U256::from(600)); + let vec = StorageVec::>::new(U256::from(300)); - // Push a nested vector - let inner_vec = vec.push(&mut sdk); + // Grow and initialize nested vectors + let inner1 = vec.grow(&mut sdk); + inner1.push(&mut sdk, U256::from(10)); + inner1.push(&mut sdk, U256::from(20)); - // Push elements to the inner vector - inner_vec.push(&mut sdk).set(&mut sdk, U256::from(10)); - inner_vec.push(&mut sdk).set(&mut sdk, U256::from(20)); + let inner2 = vec.grow(&mut sdk); + inner2.push(&mut sdk, U256::from(30)); - // Verify - assert_eq!(vec.len(&sdk), 1); + assert_eq!(vec.len(&sdk), 2); assert_eq!(vec.at(0).len(&sdk), 2); - assert_eq!(vec.at(0).at(0).get(&sdk), U256::from(10)); - assert_eq!(vec.at(0).at(1).get(&sdk), U256::from(20)); + assert_eq!(vec.at(1).len(&sdk), 1); + + // Shrink returns accessor to removed element + let removed = vec.shrink(&mut sdk).unwrap(); + assert_eq!(removed.at(0).get(&sdk), U256::from(30)); // Can still read + assert_eq!(vec.len(&sdk), 1); // But length updated } } From 66f8acc41e4e065dd868d6c9afa47f6de6db70d7 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Mon, 15 Sep 2025 10:59:48 +0400 Subject: [PATCH 14/16] refactor(sdk-derive): update storage derive macro for new API --- ...ore__storage__tests__composite_config.snap | 170 +++++++----- ...ore__storage__tests__contract_storage.snap | 173 +++++++++--- ...ore__storage__tests__nested_composite.snap | 173 +++++++++--- ...ore__storage__tests__packed_composite.snap | 258 ++++++++++-------- crates/sdk-derive/derive-core/src/storage.rs | 244 +++++++++-------- 5 files changed, 638 insertions(+), 380 deletions(-) diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__composite_config.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__composite_config.snap index e37be7365..2d39572e1 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__composite_config.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__composite_config.snap @@ -3,27 +3,27 @@ source: crates/sdk-derive/derive-core/src/storage.rs expression: formatted --- impl Config { - pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); + pub const SLOTS: usize = Self::calculate_slots(); pub fn new(slot: fluentbase_sdk::U256, offset: u8) -> Self { let mut current_slot = slot; let mut current_offset: u8 = offset; let owner_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -32,29 +32,29 @@ impl Config { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; layout }; let version_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -63,29 +63,29 @@ impl Config { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; layout }; let max_supply_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -94,7 +94,7 @@ impl Config { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; @@ -121,83 +121,95 @@ impl Config { ), } } - const fn calculate_required_slots() -> usize { + const fn calculate_slots() -> usize { let mut current_slot: usize = 0; let mut current_offset: usize = 0; { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } if current_offset > 0 { current_slot + 1 } else { current_slot } } + const fn calculate_bytes() -> usize { + let mut total_bytes: usize = 0; + total_bytes + += as fluentbase_sdk::storage::StorageLayout>::BYTES; + total_bytes + += as fluentbase_sdk::storage::StorageLayout>::BYTES; + total_bytes + += as fluentbase_sdk::storage::StorageLayout>::BYTES; + total_bytes + } ///Returns an accessor for the owner field #[inline] - pub fn owner( + pub fn owner_accessor( &self, ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { as fluentbase_sdk::storage::StorageLayout>::Accessor { as fluentbase_sdk::storage::StorageLayout>::access(self.max_supply) } } -impl fluentbase_sdk::storage::composite::CompositeStorage for Config { - const REQUIRED_SLOTS: usize = Self::REQUIRED_SLOTS; - fn from_slot(base_slot: fluentbase_sdk::U256) -> Self { - Self::new(base_slot, 0) +impl Copy for Config {} +impl Clone for Config { + fn clone(&self) -> Self { + *self + } +} +impl fluentbase_sdk::storage::StorageDescriptor for Config { + fn new(slot: fluentbase_sdk::U256, offset: u8) -> Self { + Self::new(slot, offset) + } + fn slot(&self) -> fluentbase_sdk::U256 { + self.owner.slot() + } + fn offset(&self) -> u8 { + 0 + } +} +impl fluentbase_sdk::storage::StorageLayout for Config { + type Descriptor = Self; + type Accessor = Self; + const BYTES: usize = Self::calculate_bytes(); + const SLOTS: usize = Self::calculate_slots(); + fn access(descriptor: Self::Descriptor) -> Self::Accessor { + descriptor } } diff --git a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__contract_storage.snap b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__contract_storage.snap index 551db8648..081464cf1 100644 --- a/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__contract_storage.snap +++ b/crates/sdk-derive/derive-core/src/snapshots/fluentbase_sdk_derive_core__storage__tests__contract_storage.snap @@ -3,27 +3,29 @@ source: crates/sdk-derive/derive-core/src/storage.rs expression: formatted --- impl Storage { - pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); - pub fn new(sdk: SDK, slot: fluentbase_sdk::U256, offset: u8) -> Self { + pub const SLOTS: usize = Self::calculate_slots(); + pub fn new(sdk: SDK) -> Self { + let slot = fluentbase_sdk::U256::from(0); + let offset = 0u8; let mut current_slot = slot; let mut current_offset: u8 = offset; let owner_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -32,29 +34,29 @@ impl Storage { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; layout }; let counter_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -63,7 +65,7 @@ impl Storage { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; @@ -85,52 +87,133 @@ impl Storage { ), } } - const fn calculate_required_slots() -> usize { + pub fn new_at(sdk: SDK, slot: fluentbase_sdk::U256, offset: u8) -> Self { + let mut current_slot = slot; + let mut current_offset: u8 = offset; + let owner_layout = { + let bytes = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; + let result = (current_slot, actual_offset); + current_offset += bytes; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - bytes; + current_offset = bytes; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); + current_offset = 0; + result + }; + layout + }; + let counter_layout = { + let bytes = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; + let result = (current_slot, actual_offset); + current_offset += bytes; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - bytes; + current_offset = bytes; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); + current_offset = 0; + result + }; + layout + }; + Self { + sdk, + owner: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + owner_layout.0, + owner_layout.1, + ), + counter: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + counter_layout.0, + counter_layout.1, + ), + } + } + const fn calculate_slots() -> usize { let mut current_slot: usize = 0; let mut current_offset: usize = 0; { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } @@ -138,7 +221,7 @@ impl Storage { } ///Returns an accessor for the owner field #[inline] - pub fn owner( + pub fn owner_accessor( &self, ) -> Storage { } ///Returns an accessor for the counter field #[inline] - pub fn counter( + pub fn counter_accessor( &self, ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { NestedStructTest { - pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); - pub fn new(sdk: SDK, slot: fluentbase_sdk::U256, offset: u8) -> Self { + pub const SLOTS: usize = Self::calculate_slots(); + pub fn new(sdk: SDK) -> Self { + let slot = fluentbase_sdk::U256::from(0); + let offset = 0u8; let mut current_slot = slot; let mut current_offset: u8 = offset; let counter_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -32,29 +34,29 @@ impl NestedStructTest { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; layout }; let config_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -63,7 +65,7 @@ impl NestedStructTest { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; @@ -85,52 +87,133 @@ impl NestedStructTest { ), } } - const fn calculate_required_slots() -> usize { + pub fn new_at(sdk: SDK, slot: fluentbase_sdk::U256, offset: u8) -> Self { + let mut current_slot = slot; + let mut current_offset: u8 = offset; + let counter_layout = { + let bytes = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; + let result = (current_slot, actual_offset); + current_offset += bytes; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - bytes; + current_offset = bytes; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); + current_offset = 0; + result + }; + layout + }; + let config_layout = { + let bytes = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; + let result = (current_slot, actual_offset); + current_offset += bytes; + result + } else { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + let actual_offset = 32 - bytes; + current_offset = bytes; + (current_slot, actual_offset) + } + } else { + if current_offset > 0 { + current_slot = current_slot + fluentbase_sdk::U256::from(1); + current_offset = 0; + } + let result = (current_slot, 0); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); + current_offset = 0; + result + }; + layout + }; + Self { + sdk, + counter: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + counter_layout.0, + counter_layout.1, + ), + config: < as fluentbase_sdk::storage::StorageLayout>::Descriptor as fluentbase_sdk::storage::StorageDescriptor>::new( + config_layout.0, + config_layout.1, + ), + } + } + const fn calculate_slots() -> usize { let mut current_slot: usize = 0; let mut current_offset: usize = 0; { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } @@ -138,7 +221,7 @@ impl NestedStructTest { } ///Returns an accessor for the counter field #[inline] - pub fn counter( + pub fn counter_accessor( &self, ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { NestedStructTest { } ///Returns an accessor for the config field #[inline] - pub fn config( + pub fn config_accessor( &self, ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { Self { let mut current_slot = slot; let mut current_offset: u8 = offset; let is_active_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -32,29 +32,29 @@ impl PackedConfig { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; layout }; let is_paused_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -63,29 +63,29 @@ impl PackedConfig { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; layout }; let version_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -94,29 +94,29 @@ impl PackedConfig { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; layout }; let flags_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -125,29 +125,29 @@ impl PackedConfig { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; layout }; let owner_layout = { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - let layout = if required_slots == 0 { - if current_offset + encoded_size <= 32 { - let actual_offset = 32 - current_offset - encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + let layout = if slots == 0 { + if current_offset + bytes <= 32 { + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { current_slot = current_slot + fluentbase_sdk::U256::from(1); - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { @@ -156,7 +156,7 @@ impl PackedConfig { current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; @@ -195,129 +195,145 @@ impl PackedConfig { ), } } - const fn calculate_required_slots() -> usize { + const fn calculate_slots() -> usize { let mut current_slot: usize = 0; let mut current_offset: usize = 0; { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } { - let encoded_size = as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; - if required_slots == 0 { - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + > as fluentbase_sdk::storage::StorageLayout>::SLOTS; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } if current_offset > 0 { current_slot + 1 } else { current_slot } } + const fn calculate_bytes() -> usize { + let mut total_bytes: usize = 0; + total_bytes + += as fluentbase_sdk::storage::StorageLayout>::BYTES; + total_bytes + += as fluentbase_sdk::storage::StorageLayout>::BYTES; + total_bytes + += as fluentbase_sdk::storage::StorageLayout>::BYTES; + total_bytes + += as fluentbase_sdk::storage::StorageLayout>::BYTES; + total_bytes + += as fluentbase_sdk::storage::StorageLayout>::BYTES; + total_bytes + } ///Returns an accessor for the is_active field #[inline] - pub fn is_active( + pub fn is_active_accessor( &self, ) -> as fluentbase_sdk::storage::StorageLayout>::Accessor { as fluentbase_sdk::storage::StorageLayout>::Accessor { as fluentbase_sdk::storage::StorageLayout>::Accessor { as fluentbase_sdk::storage::StorageLayout>::Accessor { as fluentbase_sdk::storage::StorageLayout>::access(self.owner) } } -impl fluentbase_sdk::storage::composite::CompositeStorage for PackedConfig { - const REQUIRED_SLOTS: usize = Self::REQUIRED_SLOTS; - fn from_slot(base_slot: fluentbase_sdk::U256) -> Self { - Self::new(base_slot, 0) +impl Copy for PackedConfig {} +impl Clone for PackedConfig { + fn clone(&self) -> Self { + *self + } +} +impl fluentbase_sdk::storage::StorageDescriptor for PackedConfig { + fn new(slot: fluentbase_sdk::U256, offset: u8) -> Self { + Self::new(slot, offset) + } + fn slot(&self) -> fluentbase_sdk::U256 { + self.is_active.slot() + } + fn offset(&self) -> u8 { + 0 + } +} +impl fluentbase_sdk::storage::StorageLayout for PackedConfig { + type Descriptor = Self; + type Accessor = Self; + const BYTES: usize = Self::calculate_bytes(); + const SLOTS: usize = Self::calculate_slots(); + fn access(descriptor: Self::Descriptor) -> Self::Accessor { + descriptor } } diff --git a/crates/sdk-derive/derive-core/src/storage.rs b/crates/sdk-derive/derive-core/src/storage.rs index 7e49640df..efbd0609c 100644 --- a/crates/sdk-derive/derive-core/src/storage.rs +++ b/crates/sdk-derive/derive-core/src/storage.rs @@ -3,68 +3,96 @@ use quote::quote; use syn::{Data, DeriveInput, Fields, GenericParam}; pub fn process_storage_layout(input: DeriveInput) -> Result { + let name = &input.ident; let has_sdk = input.generics.params.iter().any( |param| matches!(param, GenericParam::Type(type_param) if type_param.ident == "SDK"), ); - if has_sdk { - process_contract_storage(input) - } else { - process_composite_storage(input) - } -} - -fn process_contract_storage(input: DeriveInput) -> Result { - let name = &input.ident; + // Extract storage fields (excluding sdk if present) let fields = extract_storage_fields(&input)?; - let constructor = generate_constructor_body(&fields, true); - let accessors = generate_accessor_methods(&fields, true); + // Generate the same storage logic for both cases let slots_calculation = generate_const_slots_calculation(&fields); + let bytes_calculation = generate_const_bytes_calculation(&fields); + let constructor = generate_constructor_body(&fields, has_sdk); + let accessors = generate_accessor_methods(&fields); + + if has_sdk { + // Contract with SDK - only generate impl block + Ok(quote! { + impl #name { + pub const SLOTS: usize = Self::calculate_slots(); + + pub fn new(sdk: SDK) -> Self { + let slot = fluentbase_sdk::U256::from(0); + let offset = 0u8; + #constructor + } + + pub fn new_at(sdk: SDK, slot: fluentbase_sdk::U256, offset: u8) -> Self { + #constructor + } - Ok(quote! { - impl #name { - pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); + #slots_calculation + #accessors + } + }) + } else { + // Regular storage struct - generate full trait implementations + let first_field = fields.first().ok_or_else(|| { + syn::Error::new_spanned(&input, "StorageLayout requires at least one field") + })?; + let first_field_name = first_field.ident.as_ref().unwrap(); + + Ok(quote! { + impl #name { + pub const SLOTS: usize = Self::calculate_slots(); + + pub fn new(slot: fluentbase_sdk::U256, offset: u8) -> Self { + #constructor + } - pub fn new(sdk: SDK, slot: fluentbase_sdk::U256, offset: u8) -> Self { - #constructor + #slots_calculation + #bytes_calculation + #accessors } - #slots_calculation - #accessors - } - }) -} + impl Copy for #name {} -fn process_composite_storage(input: DeriveInput) -> Result { - let name = &input.ident; - let fields = extract_storage_fields(&input)?; + impl Clone for #name { + fn clone(&self) -> Self { + *self + } + } - let slots_calculation = generate_const_slots_calculation(&fields); - let constructor = generate_constructor_body(&fields, false); - let accessors = generate_accessor_methods(&fields, false); + impl fluentbase_sdk::storage::StorageDescriptor for #name { + fn new(slot: fluentbase_sdk::U256, offset: u8) -> Self { + Self::new(slot, offset) + } - Ok(quote! { - impl #name { - pub const REQUIRED_SLOTS: usize = Self::calculate_required_slots(); + fn slot(&self) -> fluentbase_sdk::U256 { + self.#first_field_name.slot() + } - pub fn new(slot: fluentbase_sdk::U256, offset: u8) -> Self { - #constructor + fn offset(&self) -> u8 { + 0 + } } - #slots_calculation - #accessors - } + impl fluentbase_sdk::storage::StorageLayout for #name { + type Descriptor = Self; + type Accessor = Self; - impl fluentbase_sdk::storage::composite::CompositeStorage for #name { - const REQUIRED_SLOTS: usize = Self::REQUIRED_SLOTS; + const BYTES: usize = Self::calculate_bytes(); + const SLOTS: usize = Self::calculate_slots(); - fn from_slot(base_slot: fluentbase_sdk::U256) -> Self { - Self::new(base_slot, 0) + fn access(descriptor: Self::Descriptor) -> Self::Accessor { + descriptor + } } - } - }) + }) + } } fn generate_constructor_body( @@ -76,11 +104,6 @@ fn generate_constructor_body( for field in fields { let field_name = field.ident.as_ref().expect("Named fields required"); - - if has_sdk && field_name == "sdk" { - continue; - } - let field_type = &field.ty; let layout_var = quote::format_ident!("{}_layout", field_name); @@ -107,38 +130,36 @@ fn generate_constructor_body( } } +// All other helper functions remain the same... fn generate_layout_calculation(layout_var: &syn::Ident, field_type: &syn::Type) -> TokenStream2 { quote! { let #layout_var = { - let encoded_size = <#field_type as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE as u8; - let required_slots = <#field_type as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let bytes = <#field_type as fluentbase_sdk::storage::StorageLayout>::BYTES as u8; + let slots = <#field_type as fluentbase_sdk::storage::StorageLayout>::SLOTS; - let layout = if required_slots == 0 { - // StoragePrimitive type - try to pack - if current_offset + encoded_size <= 32 { + let layout = if slots == 0 { + // Packable type + if current_offset + bytes <= 32 { // Fits in current slot - // Calculate offset from the RIGHT edge (Solidity packs right to left) - let actual_offset = 32 - current_offset - encoded_size; + let actual_offset = 32 - current_offset - bytes; let result = (current_slot, actual_offset); - current_offset += encoded_size; + current_offset += bytes; result } else { - // Doesn't fit, move to next slot + // Move to next slot current_slot = current_slot + fluentbase_sdk::U256::from(1); - // First element in new slot goes to rightmost position - let actual_offset = 32 - encoded_size; - current_offset = encoded_size; + let actual_offset = 32 - bytes; + current_offset = bytes; (current_slot, actual_offset) } } else { - // Composite type - needs its own slot(s) + // Full slots type if current_offset > 0 { - // If we were packing, move to next slot current_slot = current_slot + fluentbase_sdk::U256::from(1); current_offset = 0; } let result = (current_slot, 0); - current_slot = current_slot + fluentbase_sdk::U256::from(required_slots); + current_slot = current_slot + fluentbase_sdk::U256::from(slots); current_offset = 0; result }; @@ -161,42 +182,58 @@ fn generate_field_init( } } -fn generate_const_slots_calculation( +fn generate_accessor_methods( fields: &syn::punctuated::Punctuated, ) -> TokenStream2 { - let mut field_calculations = Vec::new(); - - for field in fields { + let methods = fields.iter().map(|field| { + let field_name = field.ident.as_ref().expect("Named fields required"); let field_type = &field.ty; + let doc_string = format!("Returns an accessor for the {} field", field_name); + let method_name = quote::format_ident!("{}_accessor", field_name); + + quote! { + #[doc = #doc_string] + #[inline] + pub fn #method_name(&self) -> <#field_type as fluentbase_sdk::storage::StorageLayout>::Accessor { + <#field_type as fluentbase_sdk::storage::StorageLayout>::access(self.#field_name) + } + } + }); - field_calculations.push(quote! { + quote! { #(#methods)* } +} + +fn generate_const_slots_calculation( + fields: &syn::punctuated::Punctuated, +) -> TokenStream2 { + let field_calculations = fields.iter().map(|field| { + let field_type = &field.ty; + quote! { { - let encoded_size = <#field_type as fluentbase_sdk::storage::StorageLayout>::ENCODED_SIZE; - let required_slots = <#field_type as fluentbase_sdk::storage::StorageLayout>::REQUIRED_SLOTS; + let bytes = <#field_type as fluentbase_sdk::storage::StorageLayout>::BYTES; + let slots = <#field_type as fluentbase_sdk::storage::StorageLayout>::SLOTS; - if required_slots == 0 { - // StoragePrimitive type - if current_offset + encoded_size <= 32 { - current_offset += encoded_size; + if slots == 0 { + if current_offset + bytes <= 32 { + current_offset += bytes; } else { current_slot += 1; - current_offset = encoded_size; + current_offset = bytes; } } else { - // Composite type if current_offset > 0 { current_slot += 1; current_offset = 0; } - current_slot += required_slots; + current_slot += slots; current_offset = 0; } } - }); - } + } + }); quote! { - const fn calculate_required_slots() -> usize { + const fn calculate_slots() -> usize { let mut current_slot: usize = 0; let mut current_offset: usize = 0; @@ -211,7 +248,25 @@ fn generate_const_slots_calculation( } } -// Keep the existing helper functions unchanged +fn generate_const_bytes_calculation( + fields: &syn::punctuated::Punctuated, +) -> TokenStream2 { + let field_calculations = fields.iter().map(|field| { + let field_type = &field.ty; + quote! { + total_bytes += <#field_type as fluentbase_sdk::storage::StorageLayout>::BYTES; + } + }); + + quote! { + const fn calculate_bytes() -> usize { + let mut total_bytes: usize = 0; + #(#field_calculations)* + total_bytes + } + } +} + fn extract_named_fields( input: &DeriveInput, ) -> Result<&syn::punctuated::Punctuated, syn::Error> { @@ -245,37 +300,6 @@ fn extract_storage_fields( } Ok(storage_fields) } - -fn generate_accessor_methods( - fields: &syn::punctuated::Punctuated, - has_sdk: bool, -) -> TokenStream2 { - let mut methods = Vec::new(); - - for field in fields { - let field_name = field.ident.as_ref().expect("Named fields required"); - - if has_sdk && field_name == "sdk" { - continue; - } - - let field_type = &field.ty; - let doc_string = format!("Returns an accessor for the {} field", field_name); - - methods.push(quote! { - #[doc = #doc_string] - #[inline] - pub fn #field_name(&self) -> <#field_type as fluentbase_sdk::storage::StorageLayout>::Accessor { - <#field_type as fluentbase_sdk::storage::StorageLayout>::access(self.#field_name) - } - }); - } - - quote! { - #(#methods)* - } -} - #[cfg(test)] mod tests { use super::*; From c04b6221b70ee9ccef122ed2d6cfd881b4347a0b Mon Sep 17 00:00:00 2001 From: d1r1 Date: Mon, 15 Sep 2025 11:00:11 +0400 Subject: [PATCH 15/16] refactor(examples): update examples for the new API --- examples/erc20/lib.rs | 259 ++++----------------------- examples/storage-usage/src/nested.rs | 126 +++++++------ examples/storage-usage/src/simple.rs | 122 +++++-------- examples/storage-usage/src/utils.rs | 74 +++++++- 4 files changed, 222 insertions(+), 359 deletions(-) diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index 952fbcd76..6005a061b 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -8,21 +8,9 @@ use alloc::{string::String, vec::Vec}; use alloy_sol_types::{sol, SolEvent}; use fluentbase_sdk::{ derive::{router, Storage}, - entrypoint_with_storage, - storage::{ - bytes::StorageString, - composite::Composite, - map::StorageMap, - primitive::StoragePrimitive, - BytesAccess, - MapAccess, - PrimitiveAccess, - }, - Address, - ContextReader, - SharedAPI, - B256, - U256, + basic_entrypoint, + storage::{StorageMap, StorageString, StorageU256}, + Address, ContextReader, SharedAPI, B256, U256, }; // Define the Transfer and Approval events @@ -41,38 +29,28 @@ fn emit_event(sdk: &mut SDK, event: T) { sdk.emit_log(&topics, &data); } -// Storage structures -#[derive(Storage)] -pub struct ERC20Storage { - pub token_name: StorageString, - pub token_symbol: StorageString, - pub total_supply: StoragePrimitive, - pub balances: StorageMap>, - pub allowances: StorageMap>>, -} - #[derive(Storage)] pub struct ERC20 { sdk: SDK, - storage: Composite, + token_name: StorageString, + token_symbol: StorageString, + total_supply: StorageU256, + balances: StorageMap, + allowances: StorageMap>, } #[router(mode = "solidity")] impl ERC20 { pub fn constructor(&mut self, name: String, symbol: String, initial_supply: U256) { // Set token metadata - self.storage().token_name().set_string(&mut self.sdk, &name); - self.storage() - .token_symbol() - .set_string(&mut self.sdk, &symbol); - self.storage() - .total_supply() + self.token_name_accessor().set(&mut self.sdk, &name); + self.token_symbol_accessor().set(&mut self.sdk, &symbol); + self.total_supply_accessor() .set(&mut self.sdk, initial_supply); // Assign initial supply to deployer let deployer = self.sdk.context().contract_caller(); - self.storage() - .balances() + self.balances_accessor() .entry(deployer) .set(&mut self.sdk, initial_supply); @@ -88,11 +66,11 @@ impl ERC20 { } pub fn name(&self) -> String { - self.storage().token_name().get_string(&self.sdk) + self.token_name_accessor().get(&self.sdk) } pub fn symbol(&self) -> String { - self.storage().token_symbol().get_string(&self.sdk) + self.token_symbol_accessor().get(&self.sdk) } pub fn decimals(&self) -> U256 { @@ -100,31 +78,29 @@ impl ERC20 { } pub fn total_supply(&self) -> U256 { - self.storage().total_supply().get(&self.sdk) + self.total_supply_accessor().get(&self.sdk) } pub fn balance_of(&self, account: Address) -> U256 { - self.storage().balances().entry(account).get(&self.sdk) + self.balances_accessor().entry(account).get(&self.sdk) } pub fn transfer(&mut self, to: Address, value: U256) -> U256 { let from = self.sdk.context().contract_caller(); // Check sufficient balance - let from_balance = self.storage().balances().entry(from).get(&self.sdk); + let from_balance = self.balances_accessor().entry(from).get(&self.sdk); if from_balance < value { panic!("insufficient balance"); } // Update balances - self.storage() - .balances() + self.balances_accessor() .entry(from) .set(&mut self.sdk, from_balance - value); - let to_balance = self.storage().balances().entry(to).get(&self.sdk); - self.storage() - .balances() + let to_balance = self.balances_accessor().entry(to).get(&self.sdk); + self.balances_accessor() .entry(to) .set(&mut self.sdk, to_balance + value); @@ -133,8 +109,7 @@ impl ERC20 { } pub fn allowance(&self, owner: Address, spender: Address) -> U256 { - self.storage() - .allowances() + self.allowances_accessor() .entry(owner) .entry(spender) .get(&self.sdk) @@ -143,8 +118,7 @@ impl ERC20 { pub fn approve(&mut self, spender: Address, value: U256) -> U256 { let owner = self.sdk.context().contract_caller(); - self.storage() - .allowances() + self.allowances_accessor() .entry(owner) .entry(spender) .set(&mut self.sdk, value); @@ -165,8 +139,7 @@ impl ERC20 { // Check allowance let current_allowance = self - .storage() - .allowances() + .allowances_accessor() .entry(from) .entry(spender) .get(&self.sdk); @@ -176,27 +149,24 @@ impl ERC20 { } // Check balance - let from_balance = self.storage().balances().entry(from).get(&self.sdk); + let from_balance = self.balances_accessor().entry(from).get(&self.sdk); if from_balance < value { panic!("insufficient balance"); } // Update allowance - self.storage() - .allowances() + self.allowances_accessor() .entry(from) .entry(spender) .set(&mut self.sdk, current_allowance - value); // Update balances - self.storage() - .balances() + self.balances_accessor() .entry(from) .set(&mut self.sdk, from_balance - value); - let to_balance = self.storage().balances().entry(to).get(&self.sdk); - self.storage() - .balances() + let to_balance = self.balances_accessor().entry(to).get(&self.sdk); + self.balances_accessor() .entry(to) .set(&mut self.sdk, to_balance + value); @@ -205,21 +175,12 @@ impl ERC20 { } } -entrypoint_with_storage!(ERC20); +basic_entrypoint!(ERC20); #[cfg(test)] mod tests { use super::*; - use fluentbase_sdk::{ - address, - byteorder, - bytes::Buf, - codec::Encoder, - storage::{BytesAccess, MapAccess, PrimitiveAccess}, - Address, - ContractContextV1, - U256, - }; + use fluentbase_sdk::{address, codec::Encoder, ContractContextV1, U256}; use fluentbase_sdk_testing::HostTestingContext; #[test] @@ -242,31 +203,31 @@ mod tests { }); // Act - let mut contract = ERC20::new(sdk.clone(), U256::from(0), 0); + let mut contract = ERC20::new(sdk.clone()); contract.deploy(); // Assert - verify storage was initialized correctly assert_eq!( - contract.storage().token_name().get_string(&sdk), + contract.token_name_accessor().get(&sdk), token_name, "Token name not set correctly" ); assert_eq!( - contract.storage().token_symbol().get_string(&sdk), + contract.token_symbol_accessor().get(&sdk), token_symbol, "Token symbol not set correctly" ); assert_eq!( - contract.storage().total_supply().get(&sdk), + contract.total_supply_accessor().get(&sdk), initial_supply, "Total supply not set correctly" ); // Verify deployer received initial supply assert_eq!( - contract.storage().balances().entry(deployer).get(&sdk), + contract.balances_accessor().entry(deployer).get(&sdk), initial_supply, "Deployer did not receive initial supply" ); @@ -274,7 +235,7 @@ mod tests { // Verify other addresses have zero balance let other_address = address!("2222222222222222222222222222222222222222"); assert_eq!( - contract.storage().balances().entry(other_address).get(&sdk), + contract.balances_accessor().entry(other_address).get(&sdk), U256::ZERO, "Non-deployer address should have zero balance" ); @@ -300,7 +261,7 @@ mod tests { ..Default::default() }); - let mut contract = ERC20::new(sdk.clone(), U256::from(0), 0); + let mut contract = ERC20::new(sdk.clone()); contract.deploy(); // Act & Assert - Test name() getter @@ -341,7 +302,6 @@ mod tests { "total_supply() returned incorrect value" ); - // ----> current // Test balance_of() for deployer contract.sdk = contract .sdk @@ -352,76 +312,6 @@ mod tests { balance_result.0 .0, initial_supply, "balance_of(deployer) should equal initial supply" ); - - // Test balance_of() for zero address - let zero_address = Address::ZERO; - contract.sdk = contract - .sdk - .with_input(BalanceOfCall::new((zero_address,)).encode()); - contract.main(); - let zero_balance_result = - BalanceOfReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); - assert_eq!( - zero_balance_result.0 .0, - U256::ZERO, - "balance_of(0x0) should return 0" - ); - - // Test balance_of() for random address - let random_address = address!("9999999999999999999999999999999999999999"); - contract.sdk = contract - .sdk - .with_input(BalanceOfCall::new((random_address,)).encode()); - contract.main(); - let random_balance_result = - BalanceOfReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); - assert_eq!( - random_balance_result.0 .0, - U256::ZERO, - "balance_of(random_address) should return 0" - ); - } - - #[test] - fn test_balance_of_bug() { - // Arrange - let deployer = address!("1111111111111111111111111111111111111111"); - let token_name = "TestToken".to_string(); - let token_symbol = "TST".to_string(); - let initial_supply = U256::from(10_000_000); - - // Initialize contract with constructor - let constructor_call = - ConstructorCall::new((token_name.clone(), token_symbol.clone(), initial_supply)); - - let sdk = HostTestingContext::default() - .with_input(constructor_call.encode()) - .with_contract_context(ContractContextV1 { - address: address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), - caller: deployer, - ..Default::default() - }); - - let mut contract = ERC20::new(sdk.clone(), U256::from(0), 0); - contract.deploy(); - - let balance_of_input = BalanceOfCall::new((deployer,)).encode(); - println!("{:x}", &balance_of_input); - - println!( - "Address IS_DYNAMIC: {}", -
>::IS_DYNAMIC - ); - - // Check if the tuple (Address,) is dynamic - println!( - "(Address,) IS_DYNAMIC: {}", - <(Address,) as Encoder>::IS_DYNAMIC - ); - - let decoded = BalanceOfCall::decode(&&balance_of_input.chunk()[4..]).unwrap(); - - println!("{:?}", decoded); } #[test] @@ -444,7 +334,7 @@ mod tests { ..Default::default() }); - let mut contract = ERC20::new(sdk.clone(), U256::from(0), 0); + let mut contract = ERC20::new(sdk.clone()); contract.deploy(); // Act - Transfer tokens from sender to recipient @@ -489,78 +379,5 @@ mod tests { recipient_balance.0 .0, transfer_amount, "recipient balance should equal transfer amount" ); - - // Test edge case: transfer zero amount - contract.sdk = contract - .sdk - .with_input(TransferCall::new((recipient, U256::ZERO)).encode()) - .with_contract_context(ContractContextV1 { - address: token_address, - caller: sender, - ..Default::default() - }); - - contract.main(); - let zero_transfer_result = - TransferReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); - assert_eq!( - zero_transfer_result.0 .0, - U256::from(1), - "transfer of 0 should succeed" - ); - - // Get actual current balance before transferring all - contract.sdk = contract - .sdk - .with_input(BalanceOfCall::new((sender,)).encode()); - contract.main(); - let current_sender_balance = BalanceOfReturn::decode(&&contract.sdk.take_output()[..]) - .unwrap() - .0 - .0; - - // Test transfer entire balance using the actual current balance - contract.sdk = contract - .sdk - .with_input(TransferCall::new((recipient, current_sender_balance)).encode()) - .with_contract_context(ContractContextV1 { - address: token_address, - caller: sender, - ..Default::default() - }); - - contract.main(); - let entire_balance_transfer_result = - TransferReturn::decode(&&contract.sdk.take_output()[..]).unwrap(); - assert_eq!( - entire_balance_transfer_result.0 .0, - U256::from(1), - "transfer of 0 should succeed" - ); - - // Verify sender now has 0 - contract.sdk = contract - .sdk - .with_input(BalanceOfCall::new((sender,)).encode()); - contract.main(); - let output = contract.sdk.take_output(); - let final_sender_balance = BalanceOfReturn::decode(&&output[..]).unwrap(); - - assert_eq!( - final_sender_balance.0 .0, - U256::ZERO, - "sender should have zero balance after transferring all" - ); - } - - #[test] - fn print_constructor_params_hex() { - let constructor_params = ConstructorCall::new(( - "TestToken".to_string(), - "TST".to_string(), - U256::from(1_000_000), - )) - .encode(); - println!("Constructor input: {:x}", &constructor_params); } } diff --git a/examples/storage-usage/src/nested.rs b/examples/storage-usage/src/nested.rs index 81d921bf2..f4049e3d6 100644 --- a/examples/storage-usage/src/nested.rs +++ b/examples/storage-usage/src/nested.rs @@ -2,41 +2,37 @@ use fluentbase_sdk::{ derive::Storage, storage::{ - array::StorageArray, - composite::{Composite}, - map::StorageMap, - primitive::StoragePrimitive, - vec::StorageVec, - ArrayAccess, MapAccess, PrimitiveAccess, VecAccess, + StorageAddress, StorageArray, StorageBool, StorageMap, StorageU256, StorageU32, StorageU8, + StorageVec, }, Address, SharedAPI, U256, }; // Storage structures #[derive(Storage)] -pub struct Item { - owner: StoragePrimitive
, - value: StoragePrimitive, - level: StoragePrimitive, - active: StoragePrimitive, +pub struct StorageItem { + owner: StorageAddress, + value: StorageU256, + level: StorageU8, + active: StorageBool, } #[derive(Storage)] -pub struct Inventory { - equipped_items: StorageArray, 3>, - user_items: StorageMap>, - collected_items: StorageVec>, - total_value: StoragePrimitive, - item_count: StoragePrimitive, +pub struct StorageInventory { + equipped_items: StorageArray, + user_items: StorageMap, + collected_items: StorageVec, + total_value: StorageU256, + item_count: StorageU32, } #[derive(Storage)] pub struct Game { sdk: SDK, - admin: StoragePrimitive
, - version: StoragePrimitive, - player_inventory: Composite, - is_active: StoragePrimitive, + admin: StorageAddress, + version: StorageU32, + player_inventory: StorageInventory, + is_active: StorageBool, } // Data structures for passing values @@ -47,6 +43,7 @@ pub struct ItemData { pub level: u8, pub active: bool, } + #[derive(Clone, Debug, PartialEq)] pub struct InventoryData { pub equipped_items: [ItemData; 3], @@ -57,45 +54,51 @@ pub struct InventoryData { } // Helper methods for Item -impl Item { +impl StorageItem { fn set_from(&self, data: &ItemData, sdk: &mut SDK) { - self.owner().set(sdk, data.owner); - self.value().set(sdk, data.value); - self.level().set(sdk, data.level); - self.active().set(sdk, data.active); + self.owner_accessor().set(sdk, data.owner); + self.value_accessor().set(sdk, data.value); + self.level_accessor().set(sdk, data.level); + self.active_accessor().set(sdk, data.active); } fn get_data(&self, sdk: &SDK) -> ItemData { ItemData { - owner: self.owner().get(sdk), - value: self.value().get(sdk), - level: self.level().get(sdk), - active: self.active().get(sdk), + owner: self.owner_accessor().get(sdk), + value: self.value_accessor().get(sdk), + level: self.level_accessor().get(sdk), + active: self.active_accessor().get(sdk), } } } // Helper methods for Inventory -impl Inventory { +impl StorageInventory { fn set_from(&self, data: &InventoryData, sdk: &mut SDK) { // Set equipped items for (i, item_data) in data.equipped_items.iter().enumerate() { - self.equipped_items.at(i).set_from(item_data, sdk); + self.equipped_items_accessor() + .at(i) + .set_from(item_data, sdk); } // Set user items for (user, item_data) in &data.user_items { - self.user_items.entry(*user).set_from(item_data, sdk); + self.user_items_accessor() + .entry(*user) + .set_from(item_data, sdk); } - // Set collected items + // Clear and set collected items + self.collected_items_accessor().clear(sdk); for item_data in &data.collected_items { - self.collected_items.push(sdk).set_from(item_data, sdk); + let item = self.collected_items_accessor().grow(sdk); + item.set_from(item_data, sdk); } // Set simple fields - self.total_value().set(sdk, data.total_value); - self.item_count().set(sdk, data.item_count); + self.total_value_accessor().set(sdk, data.total_value); + self.item_count_accessor().set(sdk, data.item_count); } } @@ -103,39 +106,42 @@ impl Inventory { impl Game { // Simple setters pub fn set_admin(&mut self, admin: Address) { - self.admin().set(&mut self.sdk, admin); + self.admin_accessor().set(&mut self.sdk, admin); } pub fn set_version(&mut self, version: u32) { - self.version().set(&mut self.sdk, version); + self.version_accessor().set(&mut self.sdk, version); } pub fn set_is_active(&mut self, active: bool) { - self.is_active().set(&mut self.sdk, active); + self.is_active_accessor().set(&mut self.sdk, active); } // Inventory methods pub fn set_inventory(&mut self, data: &InventoryData) { - self.player_inventory().set_from(data, &mut self.sdk); + self.player_inventory_accessor() + .set_from(data, &mut self.sdk); } pub fn set_equipped_item(&mut self, index: usize, item: &ItemData) { - self.player_inventory() - .equipped_items + self.player_inventory_accessor() + .equipped_items_accessor() .at(index) .set_from(item, &mut self.sdk); } pub fn set_user_item(&mut self, user: Address, item: &ItemData) { - self.player_inventory() - .user_items + self.player_inventory_accessor() + .user_items_accessor() .entry(user) .set_from(item, &mut self.sdk); } pub fn add_collected_item(&mut self, item: &ItemData) { - let item_descriptor = self.player_inventory().collected_items.push(&mut self.sdk); - + let item_descriptor = self + .player_inventory_accessor() + .collected_items_accessor() + .grow(&mut self.sdk); item_descriptor.set_from(item, &mut self.sdk); } } @@ -143,16 +149,14 @@ impl Game { #[cfg(test)] mod tests { use super::*; - use crate::assert_storage_layout; - use crate::nested::{Game, Inventory, Item}; - use crate::utils::storage_from_fixture; + use crate::{assert_storage_layout, utils::storage_from_fixture}; use fluentbase_sdk::address; use fluentbase_sdk_testing::HostTestingContext; #[test] fn test_layout_calculations() { assert_storage_layout! { - Item => { + StorageItem => { owner: 0, 12, value: 1, 0, level: 2, 31, @@ -162,7 +166,7 @@ mod tests { } assert_storage_layout! { - Inventory => { + StorageInventory => { equipped_items: 0, 0, user_items: 9, 0, collected_items: 10, 0, @@ -171,7 +175,16 @@ mod tests { }, total_slots: 13 } - // assert_eq!(Game::::REQUIRED_SLOTS, 15); + + assert_storage_layout! { + Game => { + admin: 0, 12, + version: 0, 8, + player_inventory: 1, 0, + is_active: 14, 31, + }, + total_slots: 15 + } } const EXPECTED_LAYOUT: &str = r#"{ @@ -206,8 +219,7 @@ mod tests { #[test] fn test_storage_layout_with_data_structures() { let sdk = HostTestingContext::default(); - - let mut game = Game::new(sdk, U256::from(0), 0); + let mut game = Game::new(sdk); // Set simple fields game.set_admin(address!("0x1111111111111111111111111111111111111111")); @@ -261,12 +273,10 @@ mod tests { total_value: U256::from(10000), item_count: 25, }; + game.set_inventory(&inventory_data); let storage = game.sdk.dump_storage(); - // print resulting storage - // println!("{}", format_storage(&storage)); - let expected_storage = storage_from_fixture(EXPECTED_LAYOUT); assert_eq!(expected_storage, storage); } diff --git a/examples/storage-usage/src/simple.rs b/examples/storage-usage/src/simple.rs index 21ecc3f26..cafaecd63 100644 --- a/examples/storage-usage/src/simple.rs +++ b/examples/storage-usage/src/simple.rs @@ -2,32 +2,25 @@ use fluentbase_sdk::{ derive::Storage, storage::{ - bytes::StorageString, composite::Composite, map::StorageMap, primitive::StoragePrimitive, - vec::StorageVec, MapAccess, PrimitiveAccess, VecAccess, + StorageAddress, StorageBool, StorageMap, StorageString, StorageU256, StorageU32, StorageVec, }, Address, SharedAPI, U256, }; -// Storage structures -#[derive(Storage)] -pub struct State { - owner: StoragePrimitive
, - counter: StoragePrimitive, - balances: StorageMap>>, - data: StorageVec>, - name: StorageString, - description: StorageString, - is_active: StoragePrimitive, - is_paused: StoragePrimitive, - is_locked: StoragePrimitive, - version: StoragePrimitive, - flags: StoragePrimitive, -} - #[derive(Storage)] pub struct App { sdk: SDK, - state: Composite, + owner: StorageAddress, + counter: StorageU256, + balances: StorageMap>, + data: StorageVec, + name: StorageString, + description: StorageString, + is_active: StorageBool, + is_paused: StorageBool, + is_locked: StorageBool, + version: StorageU32, + flags: StorageU32, } // Data structures for passing values @@ -46,88 +39,79 @@ pub struct StateData { pub flags: u32, } -// Helper methods for State -impl State { - fn set_from(&self, data: &StateData, sdk: &mut SDK) { - self.owner().set(sdk, data.owner); - self.counter().set(sdk, data.counter); +// Public API methods +impl App { + pub fn set_state(&mut self, data: &StateData) { + self.owner_accessor().set(&mut self.sdk, data.owner); + self.counter_accessor().set(&mut self.sdk, data.counter); // Set balances for (owner, token, amount) in &data.balances { - self.balances.entry(*owner).entry(*token).set(sdk, *amount); + self.balances_accessor() + .entry(*owner) + .entry(*token) + .set(&mut self.sdk, *amount); } // Set vector elements for element in &data.data_elements { - self.data.push(sdk).set(sdk, *element); + self.data_accessor().push(&mut self.sdk, *element); } // Set strings - self.name.set_string(sdk, &data.name); - self.description.set_string(sdk, &data.description); + self.name_accessor().set(&mut self.sdk, &data.name); + self.description_accessor() + .set(&mut self.sdk, &data.description); // Set packed fields - self.is_active().set(sdk, data.is_active); - self.is_paused().set(sdk, data.is_paused); - self.is_locked().set(sdk, data.is_locked); - self.version().set(sdk, data.version); - self.flags().set(sdk, data.flags); + self.is_active_accessor().set(&mut self.sdk, data.is_active); + self.is_paused_accessor().set(&mut self.sdk, data.is_paused); + self.is_locked_accessor().set(&mut self.sdk, data.is_locked); + self.version_accessor().set(&mut self.sdk, data.version); + self.flags_accessor().set(&mut self.sdk, data.flags); } - fn get_data(&self, sdk: &SDK) -> StateData { + pub fn get_state(&self) -> StateData { // Get vector elements let mut data_elements = Vec::new(); - let vec_len = self.data.len(sdk); + let vec_len = self.data_accessor().len(&self.sdk); for i in 0..vec_len { - data_elements.push(self.data.at(i).get(sdk)); + data_elements.push(self.data_accessor().at(i).get(&self.sdk)); } StateData { - owner: self.owner().get(sdk), - counter: self.counter().get(sdk), + owner: self.owner_accessor().get(&self.sdk), + counter: self.counter_accessor().get(&self.sdk), balances: vec![], // Would need to track which keys were set data_elements, - name: self.name.get_string(sdk), - description: self.description.get_string(sdk), - is_active: self.is_active().get(sdk), - is_paused: self.is_paused().get(sdk), - is_locked: self.is_locked().get(sdk), - version: self.version().get(sdk), - flags: self.flags().get(sdk), + name: self.name_accessor().get(&self.sdk), + description: self.description_accessor().get(&self.sdk), + is_active: self.is_active_accessor().get(&self.sdk), + is_paused: self.is_paused_accessor().get(&self.sdk), + is_locked: self.is_locked_accessor().get(&self.sdk), + version: self.version_accessor().get(&self.sdk), + flags: self.flags_accessor().get(&self.sdk), } } -} - -// Public API methods -impl App { - pub fn set_state(&mut self, data: &StateData) { - self.state().set_from(data, &mut self.sdk); - } - - pub fn get_state(&self) -> StateData { - self.state().get_data(&self.sdk) - } // Individual setters for convenience pub fn set_owner(&mut self, owner: Address) { - self.state().owner().set(&mut self.sdk, owner); + self.owner_accessor().set(&mut self.sdk, owner); } pub fn set_counter(&mut self, counter: U256) { - self.state().counter().set(&mut self.sdk, counter); + self.counter_accessor().set(&mut self.sdk, counter); } pub fn set_balance(&mut self, owner: Address, token: Address, amount: U256) { - self.state() - .balances + self.balances_accessor() .entry(owner) .entry(token) .set(&mut self.sdk, amount); } pub fn get_balance(&self, owner: Address, token: Address) -> U256 { - self.state() - .balances + self.balances_accessor() .entry(owner) .entry(token) .get(&self.sdk) @@ -137,15 +121,14 @@ impl App { #[cfg(test)] mod tests { use super::*; - use crate::assert_storage_layout; - use crate::utils::storage_from_fixture; + use crate::{assert_storage_layout, utils::storage_from_fixture}; use fluentbase_sdk::address; use fluentbase_sdk_testing::HostTestingContext; #[test] fn test_layout_calculations() { assert_storage_layout! { - State => { + App => { owner: 0, 12, counter: 1, 0, balances: 2, 0, @@ -183,7 +166,7 @@ mod tests { #[test] fn test_storage_layout_with_data() { let sdk = HostTestingContext::default(); - let mut app = App::new(sdk, U256::from(0), 0); + let mut app = App::new(sdk); let state_data = StateData { owner: address!("0x1111111111111111111111111111111111111111"), @@ -231,14 +214,7 @@ mod tests { // Dump and compare storage let storage = app.sdk.dump_storage(); - // Uncomment to see actual storage layout - // println!("actual:\n {}", format_storage(&storage)); - let expected_storage = storage_from_fixture(EXPECTED_LAYOUT); - - // Uncomment to see expected storage layout - // println!("expected:\n {}", format_storage(&expected_storage)); - assert_eq!(expected_storage, storage); } } diff --git a/examples/storage-usage/src/utils.rs b/examples/storage-usage/src/utils.rs index 5fa74e30d..054cc1aa7 100644 --- a/examples/storage-usage/src/utils.rs +++ b/examples/storage-usage/src/utils.rs @@ -1,9 +1,54 @@ #![allow(dead_code)] #![cfg(test)] use fluentbase_sdk::{Address, HashMap, U256}; - #[macro_export] macro_rules! assert_storage_layout { + // contracts with SDK type parameter + ( + $struct_type:ident<$sdk_type:ty> => { + $( + $field:ident: $slot:expr, $offset:expr + ),* $(,)? + }, + total_slots: $total_slots:expr + ) => { + { + use fluentbase_sdk::storage::StorageDescriptor; + use fluentbase_sdk::U256; + + // Create a default SDK instance for testing + let sdk = <$sdk_type>::default(); + let instance = $struct_type::new(sdk); + + $( + assert_eq!( + instance.$field.slot(), + U256::from($slot), + "Field '{}' slot mismatch: expected {}, got {}", + stringify!($field), + $slot, + instance.$field.slot() + ); + assert_eq!( + instance.$field.offset(), + $offset, + "Field '{}' offset mismatch: expected {}, got {}", + stringify!($field), + $offset, + instance.$field.offset() + ); + )* + + assert_eq!( + $struct_type::<$sdk_type>::SLOTS, + $total_slots, + "Total slots mismatch: expected {}, got {}", + $total_slots, + $struct_type::<$sdk_type>::SLOTS + ); + } + }; + // storage struct ( $struct_type:ty => { $( @@ -13,21 +58,36 @@ macro_rules! assert_storage_layout { total_slots: $total_slots:expr ) => { { - use fluentbase_sdk::storage::{composite::CompositeStorage, StorageDescriptor}; + use fluentbase_sdk::storage::StorageDescriptor; use fluentbase_sdk::U256; - let instance = <$struct_type>::new(U256::from(0), 0); + let instance = <$struct_type>::new(U256::from(0), 0); $( - assert_eq!(instance.$field.slot(), U256::from($slot)); - assert_eq!(instance.$field.offset(), $offset); + assert_eq!( + instance.$field.slot(), + U256::from($slot), + "Field '{}' slot mismatch: expected {}, got {}", + stringify!($field), + $slot, + instance.$field.slot() + ); + assert_eq!( + instance.$field.offset(), + $offset, + "Field '{}' offset mismatch: expected {}, got {}", + stringify!($field), + $offset, + instance.$field.offset() + ); )* assert_eq!( - <$struct_type as CompositeStorage>::REQUIRED_SLOTS, + <$struct_type>::SLOTS, $total_slots, "Total slots mismatch: expected {}, got {}", - $total_slots, <$struct_type as CompositeStorage>::REQUIRED_SLOTS + $total_slots, + <$struct_type>::SLOTS ); } }; From 4494cba591f8e416a0870f0aab81671e5080c3a5 Mon Sep 17 00:00:00 2001 From: d1r1 Date: Mon, 15 Sep 2025 11:07:48 +0400 Subject: [PATCH 16/16] docs(storage): update docs for the new version --- crates/sdk-derive/docs/storage.md | 168 +++++++++++++++++++++++++----- 1 file changed, 140 insertions(+), 28 deletions(-) diff --git a/crates/sdk-derive/docs/storage.md b/crates/sdk-derive/docs/storage.md index 0fce86249..62cd5fdc6 100644 --- a/crates/sdk-derive/docs/storage.md +++ b/crates/sdk-derive/docs/storage.md @@ -1,46 +1,158 @@ -# Solidity Storage Macro +# Storage Layout -Implements Solidity-compatible storage in Fluentbase contracts following [Solidity's storage layout specification](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html). Provides significant code size reduction through direct storage access for primitive types. +The `#[derive(Storage)]` macro implements Solidity-compatible storage layout for Fluentbase contracts, automatically +handling slot allocation and data packing according +to [Solidity's storage layout rules](https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html). -## Usage +## Basic Usage ```rust,ignore -solidity_storage! { - // Simple values - Address Owner; // Slot 0 - bool Paused; // Slot 1 +use fluentbase_sdk::{derive::Storage, storage::*}; - // Mappings - mapping(Address => U256) Balance; // Slot 2 - mapping(Address => mapping(Address => U256)) Allowance; // Slot 3 +#[derive(Storage)] +pub struct MyContract { + sdk: SDK, + owner: StorageAddress, + counter: StorageU256, + is_active: StorageBool, + balances: StorageMap, +} +``` + +## Storage Types + +### Primitive Types + +```rust,ignore +// Unsigned integers +StorageU8, StorageU16, StorageU32, StorageU64, StorageU128, StorageU256 + +// Signed integers +StorageI8, StorageI16, StorageI32, StorageI64, StorageI128 + +// Other primitives +StorageBool // 1 byte +StorageAddress // 20 bytes +StorageBytes32 // 32 bytes +``` + +### Complex Types + +```rust,ignore +// Mappings +StorageMap // Key-value storage + +// Arrays +StorageArray // Fixed-size array +StorageVec // Dynamic array + +// Strings and bytes +StorageString // Dynamic UTF-8 string +StorageBytes // Dynamic byte array +``` + +## Working with Storage + +Each field gets an accessor method that returns the appropriate interface: - // Arrays - U256[] Values; // Slot 4 +```rust,ignore +impl MyContract { + pub fn increment(&mut self) { + // Read current value + let current = self.counter_accessor().get(&self.sdk); + + // Write new value + self.counter_accessor().set(&mut self.sdk, current + U256::from(1)); + } + + pub fn set_balance(&mut self, user: Address, amount: U256) { + self.balances_accessor() + .entry(user) + .set(&mut self.sdk, amount); + } +} +``` + +## Automatic Packing + +Types smaller than 32 bytes are automatically packed into storage slots: - // Custom types (must implement Codec) - MyStruct Data; // Slot 5 +```rust,ignore +#[derive(Storage)] +pub struct PackedData { + is_active: StorageBool, // Slot 0, offset 31 + is_paused: StorageBool, // Slot 0, offset 30 + version: StorageU32, // Slot 0, offset 26 + owner: StorageAddress, // Slot 0, offset 6 + counter: StorageU256, // Slot 1, offset 0 } ``` -## Storage Key Calculation +## Nested Structures -- **Simple variables**: `key = slot` -- **Mappings**: `key = keccak256(h(k) . p)` where `h(k)` is the padded key and `p` is the slot -- **Nested mappings**: `key = keccak256(h(k2) . keccak256(h(k1) . p))` -- **Arrays**: Element at index `i` is at `keccak256(slot) + i` +Create composable storage structures: -## Direct Storage vs Encoding/Decoding +```rust,ignore +#[derive(Storage)] +pub struct Config { + version: StorageU32, + max_supply: StorageU256, +} -The macro automatically selects the most efficient storage method: +#[derive(Storage)] +pub struct Game { + sdk: SDK, + settings: Config, + players: StorageMap, +} +``` + +## Storage Slot Calculation + +The system follows Solidity's conventions: + +- **Simple values**: Stored sequentially starting from slot 0 +- **Mappings**: `slot = keccak256(key || base_slot)` +- **Dynamic arrays**: Length at base slot, elements at `keccak256(base_slot) + index` +- **Strings/bytes < 32 bytes**: Data and length in base slot +- **Strings/bytes ≥ 32 bytes**: Length in base slot, data at `keccak256(base_slot)` -- **`DirectStorage`**: For types ≤ 32 bytes (integers, booleans, addresses, small byte arrays) -- **`StorageValueSolidity`**: For complex types requiring serialization +## Complete Example -## Generated API +```rust,ignore +#[derive(Storage)] +pub struct ERC20 { + sdk: SDK, + name: StorageString, + symbol: StorageString, + total_supply: StorageU256, + balances: StorageMap, + allowances: StorageMap>, +} -For each variable `Name`, the macro generates: +impl ERC20 { + pub fn transfer(&mut self, to: Address, amount: U256) { + let from = self.sdk.context().contract_caller(); + + // Get current balances + let from_balance = self.balances_accessor().entry(from).get(&self.sdk); + let to_balance = self.balances_accessor().entry(to).get(&self.sdk); + + // Check and update + assert!(from_balance >= amount, "insufficient balance"); + + self.balances_accessor() + .entry(from) + .set(&mut self.sdk, from_balance - amount); + + self.balances_accessor() + .entry(to) + .set(&mut self.sdk, to_balance + amount); + } +} +``` -- A struct `Name` with `SLOT` constant -- Methods `get(sdk, ...)` and `set(sdk, ..., value)` +## See Also -For examples, see the [fluentbase examples repository](https://github.com/fluentlabs-xyz/fluentbase/tree/devel/examples/storage). +For working examples, check +the [Fluentbase examples repository](https://github.com/fluentlabs-xyz/fluentbase/tree/devel/examples). \ No newline at end of file