From c935e5677c18bf98bc6f0be4372552abe26f8396 Mon Sep 17 00:00:00 2001 From: austinabell Date: Fri, 7 May 2021 14:40:57 -0400 Subject: [PATCH 1/5] Setup initial API --- Cargo.lock | 5 +- near-sdk/Cargo.toml | 4 + near-sdk/src/collections/lazy/impls.rs | 91 +++++++++++++++ near-sdk/src/collections/lazy/mod.rs | 151 +++++++++++++++++++++++++ near-sdk/src/collections/mod.rs | 5 + 5 files changed, 254 insertions(+), 2 deletions(-) create mode 100644 near-sdk/src/collections/lazy/impls.rs create mode 100644 near-sdk/src/collections/lazy/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 683fb8df0..8ba2b0f78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1672,6 +1672,7 @@ dependencies = [ "near-primitives-core", "near-sdk-macros", "near-vm-logic", + "once_cell", "quickcheck", "rand 0.7.3", "rand_xorshift", @@ -1901,9 +1902,9 @@ checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" [[package]] name = "once_cell" -version = "1.7.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10acf907b94fc1b1a152d08ef97e7759650268cf986bf127f387e602b02c7e5a" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" [[package]] name = "opaque-debug" diff --git a/near-sdk/Cargo.toml b/near-sdk/Cargo.toml index e7525805f..9bf186a38 100644 --- a/near-sdk/Cargo.toml +++ b/near-sdk/Cargo.toml @@ -29,6 +29,9 @@ near-primitives-core = "=0.4.0" # Export dependencies for contracts wee_alloc = { version = "0.4.5", default-features = false, features = [] } +# Used for caching, might be worth porting only functionality needed. +once_cell = { version = "1.7.2", optional = true } + [dev-dependencies] rand = "0.7.2" trybuild = "1.0" @@ -38,3 +41,4 @@ quickcheck = "0.9.2" [features] expensive-debug = [] +unstable = ["once_cell"] diff --git a/near-sdk/src/collections/lazy/impls.rs b/near-sdk/src/collections/lazy/impls.rs new file mode 100644 index 000000000..582d24bba --- /dev/null +++ b/near-sdk/src/collections/lazy/impls.rs @@ -0,0 +1,91 @@ +use borsh::{BorshDeserialize, BorshSerialize}; + +use super::Lazy; + +impl Drop for Lazy +where + T: BorshSerialize, +{ + fn drop(&mut self) { + self.flush() + } +} + +impl core::ops::Deref for Lazy +where + T: BorshSerialize + BorshDeserialize, +{ + type Target = T; + + fn deref(&self) -> &Self::Target { + Self::get(self) + } +} + +impl core::ops::DerefMut for Lazy +where + T: BorshSerialize + BorshDeserialize, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + Self::get_mut(self) + } +} + +impl core::cmp::PartialEq for Lazy +where + T: PartialEq + BorshSerialize + BorshDeserialize, +{ + fn eq(&self, other: &Self) -> bool { + PartialEq::eq(self.get(), other.get()) + } +} + +impl core::cmp::Eq for Lazy where T: Eq + BorshSerialize + BorshDeserialize {} + +impl core::cmp::PartialOrd for Lazy +where + T: PartialOrd + BorshSerialize + BorshDeserialize, +{ + fn partial_cmp(&self, other: &Self) -> Option { + PartialOrd::partial_cmp(self.get(), other.get()) + } + fn lt(&self, other: &Self) -> bool { + PartialOrd::lt(self.get(), other.get()) + } + fn le(&self, other: &Self) -> bool { + PartialOrd::le(self.get(), other.get()) + } + fn ge(&self, other: &Self) -> bool { + PartialOrd::ge(self.get(), other.get()) + } + fn gt(&self, other: &Self) -> bool { + PartialOrd::gt(self.get(), other.get()) + } +} + +impl core::cmp::Ord for Lazy +where + T: core::cmp::Ord + BorshSerialize + BorshDeserialize, +{ + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + Ord::cmp(self.get(), other.get()) + } +} + +impl core::convert::AsRef for Lazy +where + T: BorshSerialize + BorshDeserialize, +{ + fn as_ref(&self) -> &T { + Self::get(self) + } +} + +impl core::convert::AsMut for Lazy +where + T: BorshSerialize + BorshDeserialize, +{ + fn as_mut(&mut self) -> &mut T { + Self::get_mut(self) + } +} diff --git a/near-sdk/src/collections/lazy/mod.rs b/near-sdk/src/collections/lazy/mod.rs new file mode 100644 index 000000000..4988f4e95 --- /dev/null +++ b/near-sdk/src/collections/lazy/mod.rs @@ -0,0 +1,151 @@ +//! A persistent lazy storage value. Stores a value for a given key. +//! Example: +//! If the underlying value is large, e.g. the contract needs to store an image, but it doesn't need +//! to have access to this image at regular calls, then the contract can wrap this image into +//! [`Lazy`] and it will not be deserialized until requested. + +mod impls; + +use borsh::{BorshDeserialize, BorshSerialize}; +use once_cell::unsync::OnceCell; + +use crate::env; +use crate::IntoStorageKey; + +const ERR_VALUE_SERIALIZATION: &[u8] = b"Cannot serialize value with Borsh"; +const ERR_VALUE_DESERIALIZATION: &[u8] = b"Cannot deserialize value with Borsh"; +const ERR_NOT_FOUND: &[u8] = b"No value found for the given key"; + +fn expect_key_exists(val: Option) -> T { + val.unwrap_or_else(|| env::panic(ERR_NOT_FOUND)) +} + +fn load_and_deserialize(key: &[u8]) -> T +where + T: BorshDeserialize, +{ + let bytes = expect_key_exists(env::storage_read(key)); + T::try_from_slice(&bytes).unwrap_or_else(|_| env::panic(ERR_VALUE_DESERIALIZATION)) +} + +/// An persistent lazily loaded value, that stores a value in the storage. +#[derive(BorshSerialize, BorshDeserialize)] +pub struct Lazy +where + T: BorshSerialize, +{ + /// Key bytes to index the contract's storage. + storage_key: Vec, + #[borsh_skip] + /// Cached value which is lazily loaded and deserialized from storage. + cache: OnceCell, +} + +impl Lazy +where + T: BorshSerialize, +{ + pub fn new(key: S, value: T) -> Self + where + S: IntoStorageKey, + { + Self { storage_key: key.into_storage_key(), cache: OnceCell::from(value) } + } + + /// Updates the value with a new value. This does not load the current value from storage. + pub fn set(&mut self, value: T) { + if let Some(v) = self.cache.get_mut() { + *v = value; + } else { + self.cache.set(value).ok().expect("cache is checked to not be filled above"); + } + } + + /// Writes any changes to the value to storage. This will automatically be done when the + /// value is dropped through [`Drop`] so this should only be used when the changes need to be + /// reflected in the underlying storage before then. + pub fn flush(&mut self) { + // TODO + } +} + +impl Lazy +where + T: BorshSerialize + BorshDeserialize, +{ + /// Returns a reference to the lazily loaded storage value. + /// The load from storage only happens once, and if the value is already cached, it will not + /// be reloaded. + /// + /// This function will panic if the cache is not loaded and the value at the key does not exist. + pub fn get(&self) -> &T { + self.cache.get_or_init(|| load_and_deserialize(&self.storage_key)) + } + + /// Returns a reference to the lazily loaded storage value. + /// The load from storage only happens once, and if the value is already cached, it will not + /// be reloaded. + /// + /// This function will panic if the cache is not loaded and the value at the key does not exist. + pub fn get_mut(&mut self) -> &mut T { + self.cache.get_or_init(|| load_and_deserialize(&self.storage_key)); + self.cache.get_mut().expect("cell should be filled above") + } +} + +#[cfg(not(target_arch = "wasm32"))] +#[cfg(test)] +mod tests { + use super::*; + + use crate::test_utils::test_env; + + #[test] + pub fn test_all() { + // test_env::setup(); + // let mut a = Lazy::new(b"a", None); + // assert!(a.is_none()); + // a.set(&42u32); + // assert!(a.is_some()); + // assert_eq!(a.get(), Some(42)); + // assert!(a.is_some()); + // assert_eq!(a.replace(&95), Some(42)); + // assert!(a.is_some()); + // assert_eq!(a.take(), Some(95)); + // assert!(a.is_none()); + // assert_eq!(a.replace(&105), None); + // assert!(a.is_some()); + // assert_eq!(a.get(), Some(105)); + // assert!(a.remove()); + // assert!(a.is_none()); + // assert_eq!(a.get(), None); + // assert_eq!(a.take(), None); + // assert!(a.is_none()); + } + + #[test] + pub fn test_multi() { + // test_env::setup(); + // let mut a = Lazy::new(b"a", None); + // let mut b = Lazy::new(b"b", None); + // assert!(a.is_none()); + // assert!(b.is_none()); + // a.set(&42u32); + // assert!(b.is_none()); + // assert!(a.is_some()); + // assert_eq!(a.get(), Some(42)); + // b.set(&32u32); + // assert!(a.is_some()); + // assert!(b.is_some()); + // assert_eq!(a.get(), Some(42)); + // assert_eq!(b.get(), Some(32)); + } + + #[test] + pub fn test_init_value() { + // test_env::setup(); + // let a = Lazy::new(b"a", Some(&42u32)); + // assert!(a.is_some()); + // assert_eq!(a.get(), Some(42)); + } +} diff --git a/near-sdk/src/collections/mod.rs b/near-sdk/src/collections/mod.rs index d260d2fa8..6e1456e7c 100644 --- a/near-sdk/src/collections/mod.rs +++ b/near-sdk/src/collections/mod.rs @@ -56,6 +56,11 @@ pub use unordered_set::UnorderedSet; mod lazy_option; pub use lazy_option::LazyOption; +#[cfg(feature = "unstable")] +mod lazy; +#[cfg(feature = "unstable")] +pub use lazy::Lazy; + mod tree_map; pub use tree_map::TreeMap; From f7c296f0981b0ca12f67ff77de8bde3c03b90cc5 Mon Sep 17 00:00:00 2001 From: austinabell Date: Fri, 7 May 2021 15:31:42 -0400 Subject: [PATCH 2/5] Implement correct caching/flushing and test --- near-sdk/src/collections/lazy/mod.rs | 127 +++++++++++++++------------ near-sdk/src/utils/cache_entry.rs | 69 +++++++++++++++ near-sdk/src/utils/mod.rs | 5 ++ 3 files changed, 147 insertions(+), 54 deletions(-) create mode 100644 near-sdk/src/utils/cache_entry.rs diff --git a/near-sdk/src/collections/lazy/mod.rs b/near-sdk/src/collections/lazy/mod.rs index 4988f4e95..8229eeacd 100644 --- a/near-sdk/src/collections/lazy/mod.rs +++ b/near-sdk/src/collections/lazy/mod.rs @@ -10,26 +10,42 @@ use borsh::{BorshDeserialize, BorshSerialize}; use once_cell::unsync::OnceCell; use crate::env; +use crate::utils::{CacheEntry, EntryState}; use crate::IntoStorageKey; const ERR_VALUE_SERIALIZATION: &[u8] = b"Cannot serialize value with Borsh"; const ERR_VALUE_DESERIALIZATION: &[u8] = b"Cannot deserialize value with Borsh"; const ERR_NOT_FOUND: &[u8] = b"No value found for the given key"; +const ERR_DELETED: &[u8] = b"The Lazy cell's value has been deleted. Verify the key has not been\ + deleted manually."; fn expect_key_exists(val: Option) -> T { val.unwrap_or_else(|| env::panic(ERR_NOT_FOUND)) } -fn load_and_deserialize(key: &[u8]) -> T +fn expect_consistent_state(val: Option) -> T { + val.unwrap_or_else(|| env::panic(ERR_DELETED)) +} + +fn load_and_deserialize(key: &[u8]) -> CacheEntry where T: BorshDeserialize, { let bytes = expect_key_exists(env::storage_read(key)); - T::try_from_slice(&bytes).unwrap_or_else(|_| env::panic(ERR_VALUE_DESERIALIZATION)) + let val = T::try_from_slice(&bytes).unwrap_or_else(|_| env::panic(ERR_VALUE_DESERIALIZATION)); + CacheEntry::new_cached(Some(val)) +} + +fn serialize_and_store(key: &[u8], value: &T) +where + T: BorshSerialize, +{ + let serialized = value.try_to_vec().unwrap_or_else(|_| env::panic(ERR_VALUE_SERIALIZATION)); + env::storage_write(&key, &serialized); } /// An persistent lazily loaded value, that stores a value in the storage. -#[derive(BorshSerialize, BorshDeserialize)] +#[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct Lazy where T: BorshSerialize, @@ -38,7 +54,7 @@ where storage_key: Vec, #[borsh_skip] /// Cached value which is lazily loaded and deserialized from storage. - cache: OnceCell, + cache: OnceCell>, } impl Lazy @@ -49,15 +65,21 @@ where where S: IntoStorageKey, { - Self { storage_key: key.into_storage_key(), cache: OnceCell::from(value) } + Self { + storage_key: key.into_storage_key(), + cache: OnceCell::from(CacheEntry::new_modified(Some(value))), + } } /// Updates the value with a new value. This does not load the current value from storage. pub fn set(&mut self, value: T) { if let Some(v) = self.cache.get_mut() { - *v = value; + *v.value_mut() = Some(value); } else { - self.cache.set(value).ok().expect("cache is checked to not be filled above"); + self.cache + .set(CacheEntry::new_modified(Some(value))) + .ok() + .expect("cache is checked to not be filled above"); } } @@ -65,7 +87,17 @@ where /// value is dropped through [`Drop`] so this should only be used when the changes need to be /// reflected in the underlying storage before then. pub fn flush(&mut self) { - // TODO + if let Some(v) = self.cache.get_mut() { + if v.is_modified() { + // Value was modified, serialize and put the serialized bytes in storage. + let value = expect_consistent_state(v.value().as_ref()); + serialize_and_store(&self.storage_key, value); + + // Replaces cache entry state to cached because the value in memory matches the + // stored value. This avoids writing the same value twice. + v.replace_state(EntryState::Cached); + } + } } } @@ -79,7 +111,9 @@ where /// /// This function will panic if the cache is not loaded and the value at the key does not exist. pub fn get(&self) -> &T { - self.cache.get_or_init(|| load_and_deserialize(&self.storage_key)) + let entry = self.cache.get_or_init(|| load_and_deserialize(&self.storage_key)); + + expect_consistent_state(entry.value().as_ref()) } /// Returns a reference to the lazily loaded storage value. @@ -89,7 +123,9 @@ where /// This function will panic if the cache is not loaded and the value at the key does not exist. pub fn get_mut(&mut self) -> &mut T { self.cache.get_or_init(|| load_and_deserialize(&self.storage_key)); - self.cache.get_mut().expect("cell should be filled above") + let entry = self.cache.get_mut().expect("cell should be filled above"); + + expect_consistent_state(entry.value_mut().as_mut()) } } @@ -101,51 +137,34 @@ mod tests { use crate::test_utils::test_env; #[test] - pub fn test_all() { - // test_env::setup(); - // let mut a = Lazy::new(b"a", None); - // assert!(a.is_none()); - // a.set(&42u32); - // assert!(a.is_some()); - // assert_eq!(a.get(), Some(42)); - // assert!(a.is_some()); - // assert_eq!(a.replace(&95), Some(42)); - // assert!(a.is_some()); - // assert_eq!(a.take(), Some(95)); - // assert!(a.is_none()); - // assert_eq!(a.replace(&105), None); - // assert!(a.is_some()); - // assert_eq!(a.get(), Some(105)); - // assert!(a.remove()); - // assert!(a.is_none()); - // assert_eq!(a.get(), None); - // assert_eq!(a.take(), None); - // assert!(a.is_none()); - } + pub fn test_lazy() { + test_env::setup(); + let mut a = Lazy::new(b"a", 8u32); + assert_eq!(a.get(), &8); - #[test] - pub fn test_multi() { - // test_env::setup(); - // let mut a = Lazy::new(b"a", None); - // let mut b = Lazy::new(b"b", None); - // assert!(a.is_none()); - // assert!(b.is_none()); - // a.set(&42u32); - // assert!(b.is_none()); - // assert!(a.is_some()); - // assert_eq!(a.get(), Some(42)); - // b.set(&32u32); - // assert!(a.is_some()); - // assert!(b.is_some()); - // assert_eq!(a.get(), Some(42)); - // assert_eq!(b.get(), Some(32)); - } + assert!(!env::storage_has_key(b"a")); + a.flush(); + assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 8); - #[test] - pub fn test_init_value() { - // test_env::setup(); - // let a = Lazy::new(b"a", Some(&42u32)); - // assert!(a.is_some()); - // assert_eq!(a.get(), Some(42)); + a.set(42); + + // Value in storage will still be 8 until the value is flushed + assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 8); + assert_eq!(*a, 42); + + *a = 30; + let serialized = a.try_to_vec().unwrap(); + drop(a); + assert_eq!(u32::try_from_slice(&env::storage_read(b"a").unwrap()).unwrap(), 30); + + let lazy_loaded = Lazy::::try_from_slice(&serialized).unwrap(); + assert!(lazy_loaded.cache.get().is_none()); + + let b = Lazy::new(b"b", 30); + assert!(!env::storage_has_key(b"b")); + + // A value that is not stored in storage yet and one that has not been loaded yet can + // be checked for equality. + assert_eq!(lazy_loaded, b); } } diff --git a/near-sdk/src/utils/cache_entry.rs b/near-sdk/src/utils/cache_entry.rs new file mode 100644 index 000000000..16cb922b0 --- /dev/null +++ b/near-sdk/src/utils/cache_entry.rs @@ -0,0 +1,69 @@ +#[derive(Clone, Debug)] +pub(crate) struct CacheEntry { + value: Option, + state: EntryState, +} + +impl CacheEntry { + pub fn new(value: Option, state: EntryState) -> Self { + Self { value, state } + } + + pub fn new_cached(value: Option) -> Self { + Self::new(value, EntryState::Cached) + } + + pub fn new_modified(value: Option) -> Self { + Self::new(value, EntryState::Modified) + } + + pub fn value(&self) -> &Option { + &self.value + } + + pub fn value_mut(&mut self) -> &mut Option { + self.state = EntryState::Modified; + &mut self.value + } + + #[allow(dead_code)] + pub fn into_value(self) -> Option { + self.value + } + + /// Replaces the current value with a new one. This changes the state of the cell to mutated + /// if either the old or new value is [`Some`]. + #[allow(dead_code)] + pub fn replace(&mut self, value: Option) -> Option { + let old_value = core::mem::replace(&mut self.value, value); + + if self.value.is_some() || old_value.is_some() { + // Set modified if both values are not `None` + self.state = EntryState::Modified; + } + + old_value + } + + /// Replaces the state of the cache entry and returns the previous value. + pub fn replace_state(&mut self, state: EntryState) -> EntryState { + core::mem::replace(&mut self.state, state) + } + + /// Returns true if the entry has been modified + pub fn is_modified(&self) -> bool { + matches!(self.state, EntryState::Modified) + } + + #[allow(dead_code)] + /// Returns true if the entry state has not been changed. + pub fn is_cached(&self) -> bool { + !self.is_modified() + } +} + +#[derive(Copy, Clone, Debug)] +pub(crate) enum EntryState { + Modified, + Cached, +} diff --git a/near-sdk/src/utils/mod.rs b/near-sdk/src/utils/mod.rs index 8a47b1059..bef257e0f 100644 --- a/near-sdk/src/utils/mod.rs +++ b/near-sdk/src/utils/mod.rs @@ -1,5 +1,10 @@ pub(crate) mod storage_key_impl; +#[cfg(feature = "unstable")] +mod cache_entry; +#[cfg(feature = "unstable")] +pub(crate) use cache_entry::{CacheEntry, EntryState}; + use crate::{env, AccountId, PromiseResult}; #[macro_export] From dfb6ea3b9062f03d7dec28afa61a278ba863dbe7 Mon Sep 17 00:00:00 2001 From: austinabell Date: Fri, 7 May 2021 15:46:12 -0400 Subject: [PATCH 3/5] add docs and example --- near-sdk/src/collections/lazy/mod.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/near-sdk/src/collections/lazy/mod.rs b/near-sdk/src/collections/lazy/mod.rs index 8229eeacd..ae053753c 100644 --- a/near-sdk/src/collections/lazy/mod.rs +++ b/near-sdk/src/collections/lazy/mod.rs @@ -45,6 +45,21 @@ where } /// An persistent lazily loaded value, that stores a value in the storage. +/// +/// This will only write to the underlying store if the value has changed, and will only read the +/// existing value from storage once. +/// +/// # Examples +/// ``` +/// use near_sdk::collections::Lazy; +/// +///# near_sdk::test_utils::test_env::setup(); +/// let mut a = Lazy::new(b"a", "test string".to_string()); +/// assert_eq!(*a, "test string"); +/// +/// *a = "new string".to_string(); +/// assert_eq!(a.get(), "new string"); +/// ``` #[derive(BorshSerialize, BorshDeserialize, Debug)] pub struct Lazy where From 954921bdda59d6874d0a03cec5434f37e66be052 Mon Sep 17 00:00:00 2001 From: austinabell Date: Fri, 7 May 2021 15:48:33 -0400 Subject: [PATCH 4/5] fmt --- near-sdk/src/collections/lazy/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/near-sdk/src/collections/lazy/mod.rs b/near-sdk/src/collections/lazy/mod.rs index ae053753c..18fc1f265 100644 --- a/near-sdk/src/collections/lazy/mod.rs +++ b/near-sdk/src/collections/lazy/mod.rs @@ -52,7 +52,7 @@ where /// # Examples /// ``` /// use near_sdk::collections::Lazy; -/// +/// ///# near_sdk::test_utils::test_env::setup(); /// let mut a = Lazy::new(b"a", "test string".to_string()); /// assert_eq!(*a, "test string"); From e54b448f0a281933cb1857f4997cfa845ad138d7 Mon Sep 17 00:00:00 2001 From: austinabell Date: Wed, 12 May 2021 10:03:45 -0400 Subject: [PATCH 5/5] move collection to store, update once_cell features --- near-sdk/Cargo.toml | 2 +- near-sdk/src/collections/mod.rs | 5 ----- near-sdk/src/lib.rs | 3 +++ near-sdk/src/{collections => store}/lazy/impls.rs | 0 near-sdk/src/{collections => store}/lazy/mod.rs | 0 near-sdk/src/store/mod.rs | 2 ++ 6 files changed, 6 insertions(+), 6 deletions(-) rename near-sdk/src/{collections => store}/lazy/impls.rs (100%) rename near-sdk/src/{collections => store}/lazy/mod.rs (100%) create mode 100644 near-sdk/src/store/mod.rs diff --git a/near-sdk/Cargo.toml b/near-sdk/Cargo.toml index 9bf186a38..04f08770b 100644 --- a/near-sdk/Cargo.toml +++ b/near-sdk/Cargo.toml @@ -30,7 +30,7 @@ near-primitives-core = "=0.4.0" wee_alloc = { version = "0.4.5", default-features = false, features = [] } # Used for caching, might be worth porting only functionality needed. -once_cell = { version = "1.7.2", optional = true } +once_cell = { version = "1.7.2", optional = true, default-features = false } [dev-dependencies] rand = "0.7.2" diff --git a/near-sdk/src/collections/mod.rs b/near-sdk/src/collections/mod.rs index 6e1456e7c..d260d2fa8 100644 --- a/near-sdk/src/collections/mod.rs +++ b/near-sdk/src/collections/mod.rs @@ -56,11 +56,6 @@ pub use unordered_set::UnorderedSet; mod lazy_option; pub use lazy_option::LazyOption; -#[cfg(feature = "unstable")] -mod lazy; -#[cfg(feature = "unstable")] -pub use lazy::Lazy; - mod tree_map; pub use tree_map::TreeMap; diff --git a/near-sdk/src/lib.rs b/near-sdk/src/lib.rs index 3d7237b86..3575c5a0b 100644 --- a/near-sdk/src/lib.rs +++ b/near-sdk/src/lib.rs @@ -6,6 +6,9 @@ pub use near_sdk_macros::{ serializer, BorshStorageKey, PanicOnDefault, }; +#[cfg(feature = "unstable")] +pub mod store; + pub mod collections; mod environment; pub use environment::env; diff --git a/near-sdk/src/collections/lazy/impls.rs b/near-sdk/src/store/lazy/impls.rs similarity index 100% rename from near-sdk/src/collections/lazy/impls.rs rename to near-sdk/src/store/lazy/impls.rs diff --git a/near-sdk/src/collections/lazy/mod.rs b/near-sdk/src/store/lazy/mod.rs similarity index 100% rename from near-sdk/src/collections/lazy/mod.rs rename to near-sdk/src/store/lazy/mod.rs diff --git a/near-sdk/src/store/mod.rs b/near-sdk/src/store/mod.rs new file mode 100644 index 000000000..144bb17ab --- /dev/null +++ b/near-sdk/src/store/mod.rs @@ -0,0 +1,2 @@ +mod lazy; +pub use lazy::Lazy;