From 61d564db2e0cae94860a0f7f55f012ca2fc65f0f Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 29 Sep 2021 11:01:29 -0400 Subject: [PATCH 01/36] Add `Mapping` storage collection --- crates/storage/src/collections/mapping/mod.rs | 22 +++++++++++++++++++ crates/storage/src/collections/mod.rs | 2 ++ 2 files changed, 24 insertions(+) create mode 100644 crates/storage/src/collections/mapping/mod.rs diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs new file mode 100644 index 00000000000..37a60f66633 --- /dev/null +++ b/crates/storage/src/collections/mapping/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2018-2021 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A simple mapping to contract storage. + +use core::marker::PhantomData; + +/// A mapping of key-value pairs directly into contract storage. +/// +/// If a key does not exist the `Default` value for the `value` will be returned. +pub struct Mapping(PhantomData, PhantomData); diff --git a/crates/storage/src/collections/mod.rs b/crates/storage/src/collections/mod.rs index 07ade806f2e..1875ce41b00 100644 --- a/crates/storage/src/collections/mod.rs +++ b/crates/storage/src/collections/mod.rs @@ -22,6 +22,7 @@ pub mod binary_heap; pub mod bitstash; pub mod bitvec; pub mod hashmap; +pub mod mapping; pub mod smallvec; pub mod stash; pub mod vec; @@ -32,6 +33,7 @@ pub use self::{ bitstash::BitStash, bitvec::Bitvec, hashmap::HashMap, + mapping::Mapping, stash::Stash, vec::Vec, }; From 8ae80477fba7dc0bf7268f61fcb03ffbf0ac2481 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 29 Sep 2021 16:34:28 -0400 Subject: [PATCH 02/36] Implement `insert` and `get` for `Mapping` --- crates/storage/src/collections/mapping/mod.rs | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 37a60f66633..79df117c33c 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -14,9 +14,61 @@ //! A simple mapping to contract storage. +use crate::traits::{ + ExtKeyPtr, + KeyPtr, + PackedLayout, + SpreadLayout, +}; +use ink_env::hash::{ + Blake2x256, + HashOutput, +}; +use ink_primitives::Key; + use core::marker::PhantomData; /// A mapping of key-value pairs directly into contract storage. /// /// If a key does not exist the `Default` value for the `value` will be returned. -pub struct Mapping(PhantomData, PhantomData); +pub struct Mapping { + key: Key, + mapping: (PhantomData, PhantomData), +} + +impl Mapping +where + K: scale::Encode, + V: scale::Encode + scale::Decode + Default, +{ + /// Insert the given `value` to the contract storage. + pub fn insert(&mut self, key: K, value: V) { + ink_env::set_contract_storage(&self.key(key), &value); + } + + /// Get the `value` at `key` from the contract storage. + pub fn get(&self, key: K) -> V { + ink_env::get_contract_storage(&self.key(key)) + .unwrap_or_default() + .unwrap_or_default() + } + + fn key(&self, key: K) -> Key { + let encodedable_key = (self.key, key); + let mut output = ::Type::default(); + ink_env::hash_encoded::(&encodedable_key, &mut output); + output.into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn write_to_storage_works() { + let mut m1 = Mapping::new(); + m1.insert("Hello", "World"); + assert_eq!(m1.get("Hello"), "World"); + } +} From 3a9d97e160c8908dfdf47bb46193880c800e455a Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 29 Sep 2021 17:05:42 -0400 Subject: [PATCH 03/36] Implement `SpreadLayout` for `Mapping` --- crates/storage/src/collections/mapping/mod.rs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 79df117c33c..f57ee3226cb 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -15,9 +15,11 @@ //! A simple mapping to contract storage. use crate::traits::{ + push_spread_root, + pull_spread_root, + clear_spread_root, ExtKeyPtr, KeyPtr, - PackedLayout, SpreadLayout, }; use ink_env::hash::{ @@ -33,7 +35,7 @@ use core::marker::PhantomData; /// If a key does not exist the `Default` value for the `value` will be returned. pub struct Mapping { key: Key, - mapping: (PhantomData, PhantomData), + _phatom_mapping: (PhantomData, PhantomData), } impl Mapping @@ -61,6 +63,29 @@ where } } +impl SpreadLayout for Mapping { + const FOOTPRINT: u64 = 1; + const REQUIRES_DEEP_CLEAN_UP: bool = false; + + #[inline] + fn pull_spread(ptr: &mut KeyPtr) -> Self { + let root_key = ExtKeyPtr::next_for::(ptr); + pull_spread_root::(&root_key) + } + + #[inline] + fn push_spread(&self, ptr: &mut KeyPtr) { + let root_key = ExtKeyPtr::next_for::(ptr); + push_spread_root::(self, &root_key); + } + + #[inline] + fn clear_spread(&self, ptr: &mut KeyPtr) { + let root_key = ExtKeyPtr::next_for::(ptr); + clear_spread_root::(self, &root_key); + } +} + #[cfg(test)] mod tests { use super::*; From 3e29a25d61e7dce35251b1d51fa0ef055a10eedc Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 29 Sep 2021 17:10:13 -0400 Subject: [PATCH 04/36] Fix typo --- crates/storage/src/collections/mapping/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index f57ee3226cb..5927b9f6467 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -35,7 +35,7 @@ use core::marker::PhantomData; /// If a key does not exist the `Default` value for the `value` will be returned. pub struct Mapping { key: Key, - _phatom_mapping: (PhantomData, PhantomData), + _phantom_mapping: (PhantomData, PhantomData), } impl Mapping From d1387f7e8690a5246247031ff63305b4a8e5404a Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 29 Sep 2021 17:33:09 -0400 Subject: [PATCH 05/36] Add some basic tests --- crates/storage/src/collections/mapping/mod.rs | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 5927b9f6467..44b6d68b284 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -43,6 +43,16 @@ where K: scale::Encode, V: scale::Encode + scale::Decode + Default, { + /// Creates a new empty `Mapping`. + /// + /// Not sure how this should be exposed/initialize irl. + pub fn new(key: Key) -> Self { + Self { + key, + _phantom_mapping: (Default::default(), Default::default()), + } + } + /// Insert the given `value` to the contract storage. pub fn insert(&mut self, key: K, value: V) { ink_env::set_contract_storage(&self.key(key), &value); @@ -91,9 +101,23 @@ mod tests { use super::*; #[test] - fn write_to_storage_works() { - let mut m1 = Mapping::new(); - m1.insert("Hello", "World"); - assert_eq!(m1.get("Hello"), "World"); + fn insert_and_get_work() { + ink_env::test::run_test::(|_| { + let mut mapping = Mapping::new([0u8; 32].into()); + mapping.insert(1, 2); + assert_eq!(mapping.get(1), 2); + + Ok(()) + }).unwrap() + } + + #[test] + fn gets_default_if_no_key_set() { + ink_env::test::run_test::(|_| { + let mapping: Mapping = Mapping::new([0u8; 32].into()); + assert_eq!(m1.get(1), u8::default()); + + Ok(()) + }).unwrap() } } From 9e48c183993d282943388f7c783e6ab25374c713 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 29 Sep 2021 17:35:28 -0400 Subject: [PATCH 06/36] Fix some documentation formatting --- crates/storage/src/traits/spread.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/storage/src/traits/spread.rs b/crates/storage/src/traits/spread.rs index 18179d4c999..85474ab4f9f 100644 --- a/crates/storage/src/traits/spread.rs +++ b/crates/storage/src/traits/spread.rs @@ -30,8 +30,8 @@ pub trait SpreadLayout { /// /// # Examples /// - /// An instance of type `i32` requires one storage cell, so its footprint is - /// 1. An instance of type `(i32, i32)` requires 2 storage cells since a + /// An instance of type `i32` requires one storage cell, so its footprint is 1. + /// An instance of type `(i32, i32)` requires 2 storage cells since a /// tuple or any other combined data structure always associates disjunctive /// cells for its sub types. The same applies to arrays, e.g. `[i32; 5]` /// has a footprint of 5. From b7cf4dce5d095646693d97dd932e682e15c9743a Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Thu, 30 Sep 2021 11:00:32 -0400 Subject: [PATCH 07/36] Use `PackedLayout` as trait bound instead of `Encode/Decode` --- crates/storage/src/collections/mapping/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 44b6d68b284..8ae49ea95d6 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -21,6 +21,7 @@ use crate::traits::{ ExtKeyPtr, KeyPtr, SpreadLayout, + PackedLayout, }; use ink_env::hash::{ Blake2x256, @@ -40,8 +41,8 @@ pub struct Mapping { impl Mapping where - K: scale::Encode, - V: scale::Encode + scale::Decode + Default, + K: PackedLayout, + V: PackedLayout + Default, { /// Creates a new empty `Mapping`. /// From dc87fba0b9bfe4adb40cb3507956317a42fbe299 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Thu, 30 Sep 2021 11:08:10 -0400 Subject: [PATCH 08/36] Avoid using low level `ink_env` functions when interacting with storage --- crates/storage/src/collections/mapping/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 8ae49ea95d6..e74c63dbf42 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -15,6 +15,8 @@ //! A simple mapping to contract storage. use crate::traits::{ + push_packed_root, + pull_packed_root_opt, push_spread_root, pull_spread_root, clear_spread_root, @@ -56,14 +58,12 @@ where /// Insert the given `value` to the contract storage. pub fn insert(&mut self, key: K, value: V) { - ink_env::set_contract_storage(&self.key(key), &value); + push_packed_root(&value, &self.key(key)); } /// Get the `value` at `key` from the contract storage. pub fn get(&self, key: K) -> V { - ink_env::get_contract_storage(&self.key(key)) - .unwrap_or_default() - .unwrap_or_default() + pull_packed_root_opt(&self.key(key)).unwrap_or_default() } fn key(&self, key: K) -> Key { @@ -116,7 +116,7 @@ mod tests { fn gets_default_if_no_key_set() { ink_env::test::run_test::(|_| { let mapping: Mapping = Mapping::new([0u8; 32].into()); - assert_eq!(m1.get(1), u8::default()); + assert_eq!(mapping.get(1), u8::default()); Ok(()) }).unwrap() From 728462a606f2ac9f8d99aa195f44d501c65ef4a9 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Thu, 30 Sep 2021 11:09:12 -0400 Subject: [PATCH 09/36] RustFmt --- crates/storage/src/collections/mapping/mod.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index e74c63dbf42..3bec3ec9a37 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -15,15 +15,15 @@ //! A simple mapping to contract storage. use crate::traits::{ - push_packed_root, + clear_spread_root, pull_packed_root_opt, - push_spread_root, pull_spread_root, - clear_spread_root, + push_packed_root, + push_spread_root, ExtKeyPtr, KeyPtr, - SpreadLayout, PackedLayout, + SpreadLayout, }; use ink_env::hash::{ Blake2x256, @@ -109,7 +109,8 @@ mod tests { assert_eq!(mapping.get(1), 2); Ok(()) - }).unwrap() + }) + .unwrap() } #[test] @@ -119,6 +120,7 @@ mod tests { assert_eq!(mapping.get(1), u8::default()); Ok(()) - }).unwrap() + }) + .unwrap() } } From 5b2a8afcf37edcb490d6682d994a90cf2d1fe267 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Thu, 30 Sep 2021 11:10:16 -0400 Subject: [PATCH 10/36] Appease Clippy --- crates/storage/src/collections/mapping/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 3bec3ec9a37..c11251b2a7e 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -81,19 +81,19 @@ impl SpreadLayout for Mapping { #[inline] fn pull_spread(ptr: &mut KeyPtr) -> Self { let root_key = ExtKeyPtr::next_for::(ptr); - pull_spread_root::(&root_key) + pull_spread_root::(root_key) } #[inline] fn push_spread(&self, ptr: &mut KeyPtr) { let root_key = ExtKeyPtr::next_for::(ptr); - push_spread_root::(self, &root_key); + push_spread_root::(self, root_key); } #[inline] fn clear_spread(&self, ptr: &mut KeyPtr) { let root_key = ExtKeyPtr::next_for::(ptr); - clear_spread_root::(self, &root_key); + clear_spread_root::(self, root_key); } } From a5d016e4944c09cda81c16c1efdd4ddd975f0ccb Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Thu, 30 Sep 2021 15:00:07 -0400 Subject: [PATCH 11/36] Only use single `PhantomData` field --- crates/storage/src/collections/mapping/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index c11251b2a7e..769cfec2052 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -38,7 +38,7 @@ use core::marker::PhantomData; /// If a key does not exist the `Default` value for the `value` will be returned. pub struct Mapping { key: Key, - _phantom_mapping: (PhantomData, PhantomData), + _marker: PhantomData<(K, V)>, } impl Mapping @@ -52,7 +52,7 @@ where pub fn new(key: Key) -> Self { Self { key, - _phantom_mapping: (Default::default(), Default::default()), + _marker: Default::default(), } } From ad524c60377aae1e92e305ce00fc266d2971e512 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Thu, 30 Sep 2021 15:06:57 -0400 Subject: [PATCH 12/36] Change `get` API to take reference to `key` --- crates/storage/src/collections/mapping/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 769cfec2052..d03c79f02e2 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -58,15 +58,15 @@ where /// Insert the given `value` to the contract storage. pub fn insert(&mut self, key: K, value: V) { - push_packed_root(&value, &self.key(key)); + push_packed_root(&value, &self.key(&key)); } /// Get the `value` at `key` from the contract storage. - pub fn get(&self, key: K) -> V { + pub fn get(&self, key: &K) -> V { pull_packed_root_opt(&self.key(key)).unwrap_or_default() } - fn key(&self, key: K) -> Key { + fn key(&self, key: &K) -> Key { let encodedable_key = (self.key, key); let mut output = ::Type::default(); ink_env::hash_encoded::(&encodedable_key, &mut output); @@ -106,7 +106,7 @@ mod tests { ink_env::test::run_test::(|_| { let mut mapping = Mapping::new([0u8; 32].into()); mapping.insert(1, 2); - assert_eq!(mapping.get(1), 2); + assert_eq!(mapping.get(&1), 2); Ok(()) }) @@ -117,7 +117,7 @@ mod tests { fn gets_default_if_no_key_set() { ink_env::test::run_test::(|_| { let mapping: Mapping = Mapping::new([0u8; 32].into()); - assert_eq!(mapping.get(1), u8::default()); + assert_eq!(mapping.get(&1), u8::default()); Ok(()) }) From e6293a3b5a2a47cb521579ad8dcd71261339e7ba Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Thu, 30 Sep 2021 15:36:24 -0400 Subject: [PATCH 13/36] Implement `TypeInfo` and `StorageLayout` for `Mapping` --- crates/storage/src/collections/mapping/mod.rs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index d03c79f02e2..c9a9fe73075 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -24,18 +24,27 @@ use crate::traits::{ KeyPtr, PackedLayout, SpreadLayout, + StorageLayout, }; + +use core::marker::PhantomData; + use ink_env::hash::{ Blake2x256, HashOutput, }; use ink_primitives::Key; +use ink_metadata::layout::{ + CellLayout, + Layout, + LayoutKey, +}; -use core::marker::PhantomData; /// A mapping of key-value pairs directly into contract storage. /// /// If a key does not exist the `Default` value for the `value` will be returned. +#[derive(scale_info::TypeInfo)] pub struct Mapping { key: Key, _marker: PhantomData<(K, V)>, @@ -97,6 +106,19 @@ impl SpreadLayout for Mapping { } } + +impl StorageLayout for Mapping +where + K: scale_info::TypeInfo + 'static, + V: scale_info::TypeInfo + 'static, +{ + fn layout(key_ptr: &mut KeyPtr) -> Layout { + Layout::Cell(CellLayout::new::(LayoutKey::from( + key_ptr.advance_by(1), + ))) + } +} + #[cfg(test)] mod tests { use super::*; From b55a24ecf6daa17bf93cfeae3b72d42decf911d7 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Thu, 30 Sep 2021 16:05:49 -0400 Subject: [PATCH 14/36] Properly gate `TypeInfo` and `StorageLayout` impls behind `std` --- crates/storage/src/collections/mapping/mod.rs | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index c9a9fe73075..dc4eefdd1e4 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -24,7 +24,6 @@ use crate::traits::{ KeyPtr, PackedLayout, SpreadLayout, - StorageLayout, }; use core::marker::PhantomData; @@ -34,17 +33,11 @@ use ink_env::hash::{ HashOutput, }; use ink_primitives::Key; -use ink_metadata::layout::{ - CellLayout, - Layout, - LayoutKey, -}; - /// A mapping of key-value pairs directly into contract storage. /// /// If a key does not exist the `Default` value for the `value` will be returned. -#[derive(scale_info::TypeInfo)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] pub struct Mapping { key: Key, _marker: PhantomData<(K, V)>, @@ -106,18 +99,27 @@ impl SpreadLayout for Mapping { } } - -impl StorageLayout for Mapping -where - K: scale_info::TypeInfo + 'static, - V: scale_info::TypeInfo + 'static, -{ - fn layout(key_ptr: &mut KeyPtr) -> Layout { +#[cfg(feature = "std")] +const _: () = { + use crate::traits::StorageLayout; + use ink_metadata::layout::{ + CellLayout, + Layout, + LayoutKey, + }; + + impl StorageLayout for Mapping + where + K: scale_info::TypeInfo + 'static, + V: scale_info::TypeInfo + 'static, + { + fn layout(key_ptr: &mut KeyPtr) -> Layout { Layout::Cell(CellLayout::new::(LayoutKey::from( key_ptr.advance_by(1), ))) - } -} + } + } +}; #[cfg(test)] mod tests { From 9c6f853f6b28fe786edcbb0d761b6ed5bc6391c5 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Thu, 30 Sep 2021 16:30:04 -0400 Subject: [PATCH 15/36] Replace `HashMap` with `Mapping` in ERC-20 example --- examples/erc20/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index 755be1d6bbf..da5b49986b6 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -6,7 +6,7 @@ use ink_lang as ink; mod erc20 { #[cfg(not(feature = "ink-as-dependency"))] use ink_storage::{ - collections::HashMap as StorageHashMap, + collections::mapping::Mapping, lazy::Lazy, }; @@ -16,10 +16,10 @@ mod erc20 { /// Total token supply. total_supply: Lazy, /// Mapping from owner to number of owned token. - balances: StorageHashMap, + balances: Mapping, /// Mapping of the token amount which an account is allowed to withdraw /// from another account. - allowances: StorageHashMap<(AccountId, AccountId), Balance>, + allowances: Mapping<(AccountId, AccountId), Balance>, } /// Event emitted when a token transfer occurs. @@ -61,12 +61,12 @@ mod erc20 { #[ink(constructor)] pub fn new(initial_supply: Balance) -> Self { let caller = Self::env().caller(); - let mut balances = StorageHashMap::new(); + let mut balances = Mapping::new([1u8; 32].into()); balances.insert(caller, initial_supply); let instance = Self { total_supply: Lazy::new(initial_supply), balances, - allowances: StorageHashMap::new(), + allowances: Mapping::new([1u8; 32].into()), }; Self::env().emit_event(Transfer { from: None, @@ -87,7 +87,7 @@ mod erc20 { /// Returns `0` if the account is non-existent. #[ink(message)] pub fn balance_of(&self, owner: AccountId) -> Balance { - self.balances.get(&owner).copied().unwrap_or(0) + self.balances.get(&owner) //.copied().unwrap_or(0) } /// Returns the amount which `spender` is still allowed to withdraw from `owner`. @@ -95,7 +95,7 @@ mod erc20 { /// Returns `0` if no allowance has been set `0`. #[ink(message)] pub fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance { - self.allowances.get(&(owner, spender)).copied().unwrap_or(0) + self.allowances.get(&(owner, spender)) //.copied().unwrap_or(0) } /// Transfers `value` amount of tokens from the caller's account to account `to`. From 299ba860c6c3301a5f974e9701ffce87a3b1aa11 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Fri, 1 Oct 2021 10:48:04 -0400 Subject: [PATCH 16/36] Return `Option` from `Mapping::get` --- crates/storage/src/collections/mapping/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index dc4eefdd1e4..b83c3d338e7 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -46,7 +46,7 @@ pub struct Mapping { impl Mapping where K: PackedLayout, - V: PackedLayout + Default, + V: PackedLayout, { /// Creates a new empty `Mapping`. /// @@ -64,8 +64,8 @@ where } /// Get the `value` at `key` from the contract storage. - pub fn get(&self, key: &K) -> V { - pull_packed_root_opt(&self.key(key)).unwrap_or_default() + pub fn get(&self, key: &K) -> Option { + pull_packed_root_opt(&self.key(key)) } fn key(&self, key: &K) -> Key { @@ -130,7 +130,7 @@ mod tests { ink_env::test::run_test::(|_| { let mut mapping = Mapping::new([0u8; 32].into()); mapping.insert(1, 2); - assert_eq!(mapping.get(&1), 2); + assert_eq!(mapping.get(&1), Some(2)); Ok(()) }) @@ -141,7 +141,7 @@ mod tests { fn gets_default_if_no_key_set() { ink_env::test::run_test::(|_| { let mapping: Mapping = Mapping::new([0u8; 32].into()); - assert_eq!(mapping.get(&1), u8::default()); + assert_eq!(mapping.get(&1), None); Ok(()) }) From 78e69f3a2810d67740ce8c94cda321452852ea81 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Fri, 1 Oct 2021 10:56:47 -0400 Subject: [PATCH 17/36] Update ERC-20 to handle `Option` returns --- crates/storage/src/collections/mapping/mod.rs | 2 ++ examples/erc20/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index b83c3d338e7..fbc8ba02ded 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -64,6 +64,8 @@ where } /// Get the `value` at `key` from the contract storage. + /// + /// Returns `None` if no `value` exists at the given `key`. pub fn get(&self, key: &K) -> Option { pull_packed_root_opt(&self.key(key)) } diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index da5b49986b6..e71290fd7e2 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -87,7 +87,7 @@ mod erc20 { /// Returns `0` if the account is non-existent. #[ink(message)] pub fn balance_of(&self, owner: AccountId) -> Balance { - self.balances.get(&owner) //.copied().unwrap_or(0) + self.balances.get(&owner).unwrap_or_default() // .copied().unwrap_or(0) } /// Returns the amount which `spender` is still allowed to withdraw from `owner`. @@ -95,7 +95,7 @@ mod erc20 { /// Returns `0` if no allowance has been set `0`. #[ink(message)] pub fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance { - self.allowances.get(&(owner, spender)) //.copied().unwrap_or(0) + self.allowances.get(&(owner, spender)).unwrap_or_default() //.copied().unwrap_or(0) } /// Transfers `value` amount of tokens from the caller's account to account `to`. From 7c55dbff8ea2edf2b21d7f03a6b0f24c6319784d Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Mon, 11 Oct 2021 16:40:15 +0100 Subject: [PATCH 18/36] Change `get` and `key` to use `Borrow`-ed values --- crates/storage/src/collections/mapping/mod.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index fbc8ba02ded..a918342ca6b 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -66,12 +66,20 @@ where /// Get the `value` at `key` from the contract storage. /// /// Returns `None` if no `value` exists at the given `key`. - pub fn get(&self, key: &K) -> Option { + pub fn get(&self, key: &Q) -> Option + where + K: core::borrow::Borrow, + Q: scale::Encode, + { pull_packed_root_opt(&self.key(key)) } - fn key(&self, key: &K) -> Key { - let encodedable_key = (self.key, key); + fn key(&self, key: &Q) -> Key + where + K: core::borrow::Borrow, + Q: scale::Encode, + { + let encodedable_key = (&self.key, key); let mut output = ::Type::default(); ink_env::hash_encoded::(&encodedable_key, &mut output); output.into() From 84576ed9cffb5cbe54372418d370ee822e77937f Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Mon, 11 Oct 2021 17:06:44 +0100 Subject: [PATCH 19/36] Add `Debug` and `Default` implementations --- crates/storage/src/collections/mapping/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index a918342ca6b..f754f93e118 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -38,11 +38,18 @@ use ink_primitives::Key; /// /// If a key does not exist the `Default` value for the `value` will be returned. #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[derive(Default)] pub struct Mapping { key: Key, _marker: PhantomData<(K, V)>, } +impl core::fmt::Debug for Mapping { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("Mapping").field("key", &self.key).finish() + } +} + impl Mapping where K: PackedLayout, From a5d01e4fd6d6f18387b4642e8afeece80f976079 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Mon, 11 Oct 2021 17:29:24 +0100 Subject: [PATCH 20/36] Proper spelling --- crates/storage/src/collections/mapping/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index f754f93e118..f10d757c99b 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -57,7 +57,7 @@ where { /// Creates a new empty `Mapping`. /// - /// Not sure how this should be exposed/initialize irl. + /// Not sure how this should be exposed/initialize in real life. pub fn new(key: Key) -> Self { Self { key, From 14c4347983ba50366b34d4a400ea49f74cbbce75 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 13 Oct 2021 10:59:28 +0100 Subject: [PATCH 21/36] Change `insert` to only accept borrowed K,V pairs --- crates/storage/src/collections/mapping/mod.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index f10d757c99b..fd253f5ee8f 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -66,8 +66,14 @@ where } /// Insert the given `value` to the contract storage. - pub fn insert(&mut self, key: K, value: V) { - push_packed_root(&value, &self.key(&key)); + pub fn insert(&mut self, key: &Q, value: &R) + where + K: core::borrow::Borrow, + Q: scale::Encode, + V: core::borrow::Borrow, + R: PackedLayout, + { + push_packed_root(value, &self.key(key)); } /// Get the `value` at `key` from the contract storage. @@ -145,8 +151,8 @@ mod tests { #[test] fn insert_and_get_work() { ink_env::test::run_test::(|_| { - let mut mapping = Mapping::new([0u8; 32].into()); - mapping.insert(1, 2); + let mut mapping: Mapping = Mapping::new([0u8; 32].into()); + mapping.insert(&1, &2); assert_eq!(mapping.get(&1), Some(2)); Ok(()) From 5508fc172c55dba13e6296f7c879017596b21f04 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 13 Oct 2021 11:00:10 +0100 Subject: [PATCH 22/36] Update ERC-20 example accordingly --- examples/erc20/lib.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index e71290fd7e2..74f56baaacc 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -62,7 +62,7 @@ mod erc20 { pub fn new(initial_supply: Balance) -> Self { let caller = Self::env().caller(); let mut balances = Mapping::new([1u8; 32].into()); - balances.insert(caller, initial_supply); + balances.insert(&caller, &initial_supply); let instance = Self { total_supply: Lazy::new(initial_supply), balances, @@ -121,7 +121,7 @@ mod erc20 { #[ink(message)] pub fn approve(&mut self, spender: AccountId, value: Balance) -> Result<()> { let owner = self.env().caller(); - self.allowances.insert((owner, spender), value); + self.allowances.insert(&(owner, spender), &value); self.env().emit_event(Approval { owner, spender, @@ -157,7 +157,7 @@ mod erc20 { return Err(Error::InsufficientAllowance) } self.transfer_from_to(from, to, value)?; - self.allowances.insert((from, caller), allowance - value); + self.allowances.insert(&(from, caller), &(allowance - value)); Ok(()) } @@ -179,9 +179,9 @@ mod erc20 { if from_balance < value { return Err(Error::InsufficientBalance) } - self.balances.insert(from, from_balance - value); + self.balances.insert(&from, &(from_balance - value)); let to_balance = self.balance_of(to); - self.balances.insert(to, to_balance + value); + self.balances.insert(&to, &(to_balance + value)); self.env().emit_event(Transfer { from: Some(from), to: Some(to), From baa2c32d6b6831a7d07d0f2b84a4fe69eee4c354 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 13 Oct 2021 11:27:36 +0100 Subject: [PATCH 23/36] Make more explicit what each `key` is referring to --- crates/storage/src/collections/mapping/mod.rs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index fd253f5ee8f..7b71d1fe086 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -40,13 +40,13 @@ use ink_primitives::Key; #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] #[derive(Default)] pub struct Mapping { - key: Key, + offset_key: Key, _marker: PhantomData<(K, V)>, } impl core::fmt::Debug for Mapping { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.debug_struct("Mapping").field("key", &self.key).finish() + f.debug_struct("Mapping").field("offset_key", &self.offset_key).finish() } } @@ -58,9 +58,9 @@ where /// Creates a new empty `Mapping`. /// /// Not sure how this should be exposed/initialize in real life. - pub fn new(key: Key) -> Self { + pub fn new(offset_key: Key) -> Self { Self { - key, + offset_key, _marker: Default::default(), } } @@ -73,7 +73,7 @@ where V: core::borrow::Borrow, R: PackedLayout, { - push_packed_root(value, &self.key(key)); + push_packed_root(value, &self.storage_key(key)); } /// Get the `value` at `key` from the contract storage. @@ -84,15 +84,19 @@ where K: core::borrow::Borrow, Q: scale::Encode, { - pull_packed_root_opt(&self.key(key)) + pull_packed_root_opt(&self.storage_key(key)) } - fn key(&self, key: &Q) -> Key + /// Returns a `Key` pointer used internally by the storage API. + /// + /// This key is a combination of the `Mapping`'s internal `offset_key` and the user provided + /// `key`. + fn storage_key(&self, key: &Q) -> Key where K: core::borrow::Borrow, Q: scale::Encode, { - let encodedable_key = (&self.key, key); + let encodedable_key = (&self.offset_key, key); let mut output = ::Type::default(); ink_env::hash_encoded::(&encodedable_key, &mut output); output.into() From cede0330f1f60645f19a825801a806fe39d2b0be Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 13 Oct 2021 11:57:41 +0100 Subject: [PATCH 24/36] Try using a `RefCell` instead of passing `Key` around --- crates/storage/src/collections/mapping/mod.rs | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 7b71d1fe086..3d899271dab 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -26,7 +26,10 @@ use crate::traits::{ SpreadLayout, }; -use core::marker::PhantomData; +use core::{ + cell::RefCell, + marker::PhantomData, +}; use ink_env::hash::{ Blake2x256, @@ -41,12 +44,16 @@ use ink_primitives::Key; #[derive(Default)] pub struct Mapping { offset_key: Key, + storage_key: RefCell, _marker: PhantomData<(K, V)>, } impl core::fmt::Debug for Mapping { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.debug_struct("Mapping").field("offset_key", &self.offset_key).finish() + f.debug_struct("Mapping") + .field("offset_key", &self.offset_key) + .field("storage_key", &self.storage_key) + .finish() } } @@ -61,6 +68,7 @@ where pub fn new(offset_key: Key) -> Self { Self { offset_key, + storage_key: RefCell::new(Key::default()), _marker: Default::default(), } } @@ -73,7 +81,8 @@ where V: core::borrow::Borrow, R: PackedLayout, { - push_packed_root(value, &self.storage_key(key)); + self.update_storage_key(key); + push_packed_root(value, &self.storage_key.borrow()); } /// Get the `value` at `key` from the contract storage. @@ -84,14 +93,17 @@ where K: core::borrow::Borrow, Q: scale::Encode, { - pull_packed_root_opt(&self.storage_key(key)) + self.update_storage_key(key); + pull_packed_root_opt(&self.storage_key.borrow()) } - /// Returns a `Key` pointer used internally by the storage API. + /// Updates the `Key` pointer used internally by the storage API. /// /// This key is a combination of the `Mapping`'s internal `offset_key` and the user provided /// `key`. - fn storage_key(&self, key: &Q) -> Key + /// + /// Callers should be careful to update the key before making any queries to storage. + fn update_storage_key(&self, key: &Q) where K: core::borrow::Borrow, Q: scale::Encode, @@ -99,7 +111,7 @@ where let encodedable_key = (&self.offset_key, key); let mut output = ::Type::default(); ink_env::hash_encoded::(&encodedable_key, &mut output); - output.into() + *self.storage_key.borrow_mut() = output.into(); } } From e5c6ef51a6401c721ca47331c6c6dc9b7c5d1419 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Wed, 13 Oct 2021 14:39:38 +0100 Subject: [PATCH 25/36] Try using `UnsafeCell` instead --- crates/storage/src/collections/mapping/mod.rs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 3d899271dab..2ad98d6d226 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -30,6 +30,7 @@ use core::{ cell::RefCell, marker::PhantomData, }; +use core::cell::UnsafeCell; use ink_env::hash::{ Blake2x256, @@ -44,7 +45,7 @@ use ink_primitives::Key; #[derive(Default)] pub struct Mapping { offset_key: Key, - storage_key: RefCell, + storage_key: UnsafeCell, _marker: PhantomData<(K, V)>, } @@ -68,7 +69,7 @@ where pub fn new(offset_key: Key) -> Self { Self { offset_key, - storage_key: RefCell::new(Key::default()), + storage_key: UnsafeCell::new(Key::default()), _marker: Default::default(), } } @@ -82,7 +83,11 @@ where R: PackedLayout, { self.update_storage_key(key); - push_packed_root(value, &self.storage_key.borrow()); + let ptr = self.storage_key.get(); + let ptr_ref = unsafe { + ptr.as_ref().expect("Robin, this is safe, trust me.") + }; + push_packed_root(value, ptr_ref); } /// Get the `value` at `key` from the contract storage. @@ -94,7 +99,11 @@ where Q: scale::Encode, { self.update_storage_key(key); - pull_packed_root_opt(&self.storage_key.borrow()) + let ptr = self.storage_key.get(); + let ptr_ref = unsafe { + ptr.as_ref().expect("Robin, this is safe, trust me.") + }; + pull_packed_root_opt(ptr_ref) } /// Updates the `Key` pointer used internally by the storage API. @@ -111,7 +120,11 @@ where let encodedable_key = (&self.offset_key, key); let mut output = ::Type::default(); ink_env::hash_encoded::(&encodedable_key, &mut output); - *self.storage_key.borrow_mut() = output.into(); + + // "Mr. Compiler, this is safe, trust me." + unsafe { + *self.storage_key.get() = output.into(); + } } } From 013597742020979435e837ef30511623bbceff69 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Thu, 14 Oct 2021 11:39:51 +0100 Subject: [PATCH 26/36] Revert "Try using a `RefCell` instead of passing `Key` around" This reverts commit cede0330f1f60645f19a825801a806fe39d2b0be. Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than what we have now, and it introduced `unsafe` code. We believe the limiting factor here is the `Key` type definition anyways. --- crates/storage/src/collections/mapping/mod.rs | 39 ++++--------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 2ad98d6d226..7b71d1fe086 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -26,11 +26,7 @@ use crate::traits::{ SpreadLayout, }; -use core::{ - cell::RefCell, - marker::PhantomData, -}; -use core::cell::UnsafeCell; +use core::marker::PhantomData; use ink_env::hash::{ Blake2x256, @@ -45,16 +41,12 @@ use ink_primitives::Key; #[derive(Default)] pub struct Mapping { offset_key: Key, - storage_key: UnsafeCell, _marker: PhantomData<(K, V)>, } impl core::fmt::Debug for Mapping { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.debug_struct("Mapping") - .field("offset_key", &self.offset_key) - .field("storage_key", &self.storage_key) - .finish() + f.debug_struct("Mapping").field("offset_key", &self.offset_key).finish() } } @@ -69,7 +61,6 @@ where pub fn new(offset_key: Key) -> Self { Self { offset_key, - storage_key: UnsafeCell::new(Key::default()), _marker: Default::default(), } } @@ -82,12 +73,7 @@ where V: core::borrow::Borrow, R: PackedLayout, { - self.update_storage_key(key); - let ptr = self.storage_key.get(); - let ptr_ref = unsafe { - ptr.as_ref().expect("Robin, this is safe, trust me.") - }; - push_packed_root(value, ptr_ref); + push_packed_root(value, &self.storage_key(key)); } /// Get the `value` at `key` from the contract storage. @@ -98,21 +84,14 @@ where K: core::borrow::Borrow, Q: scale::Encode, { - self.update_storage_key(key); - let ptr = self.storage_key.get(); - let ptr_ref = unsafe { - ptr.as_ref().expect("Robin, this is safe, trust me.") - }; - pull_packed_root_opt(ptr_ref) + pull_packed_root_opt(&self.storage_key(key)) } - /// Updates the `Key` pointer used internally by the storage API. + /// Returns a `Key` pointer used internally by the storage API. /// /// This key is a combination of the `Mapping`'s internal `offset_key` and the user provided /// `key`. - /// - /// Callers should be careful to update the key before making any queries to storage. - fn update_storage_key(&self, key: &Q) + fn storage_key(&self, key: &Q) -> Key where K: core::borrow::Borrow, Q: scale::Encode, @@ -120,11 +99,7 @@ where let encodedable_key = (&self.offset_key, key); let mut output = ::Type::default(); ink_env::hash_encoded::(&encodedable_key, &mut output); - - // "Mr. Compiler, this is safe, trust me." - unsafe { - *self.storage_key.get() = output.into(); - } + output.into() } } From 644ab716b4d7f8b10aa887e865ba5b32b8fd6f05 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Tue, 19 Oct 2021 17:39:22 +0100 Subject: [PATCH 27/36] Clean up some of the documentation --- crates/storage/src/collections/mapping/mod.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 7b71d1fe086..46e94045774 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -13,6 +13,9 @@ // limitations under the License. //! A simple mapping to contract storage. +//! +//! This mapping doesn't actually "own" any data. Instead it is just a simple wrapper around the +//! contract storage facilities. use crate::traits::{ clear_spread_root, @@ -35,8 +38,6 @@ use ink_env::hash::{ use ink_primitives::Key; /// A mapping of key-value pairs directly into contract storage. -/// -/// If a key does not exist the `Default` value for the `value` will be returned. #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] #[derive(Default)] pub struct Mapping { @@ -46,7 +47,9 @@ pub struct Mapping { impl core::fmt::Debug for Mapping { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { - f.debug_struct("Mapping").field("offset_key", &self.offset_key).finish() + f.debug_struct("Mapping") + .field("offset_key", &self.offset_key) + .finish() } } @@ -57,7 +60,8 @@ where { /// Creates a new empty `Mapping`. /// - /// Not sure how this should be exposed/initialize in real life. + /// TODO [#961]: Ideally we improve how this is initialized by extending the + /// `SpreadLayout`/`PackedLayout` traits for non-caching data structures. pub fn new(offset_key: Key) -> Self { Self { offset_key, From b95416227cd9812c14b6c3f7f9e016d365e638b8 Mon Sep 17 00:00:00 2001 From: Robin Freyler Date: Tue, 9 Nov 2021 16:44:28 +0100 Subject: [PATCH 28/36] Simple Mapping type improvements (#979) * Add `Mapping` storage collection * Implement `insert` and `get` for `Mapping` * Implement `SpreadLayout` for `Mapping` * Fix typo * Add some basic tests * Fix some documentation formatting * Use `PackedLayout` as trait bound instead of `Encode/Decode` * Avoid using low level `ink_env` functions when interacting with storage * RustFmt * Appease Clippy * Only use single `PhantomData` field * Change `get` API to take reference to `key` * Implement `TypeInfo` and `StorageLayout` for `Mapping` * Properly gate `TypeInfo` and `StorageLayout` impls behind `std` * Replace `HashMap` with `Mapping` in ERC-20 example * Return `Option` from `Mapping::get` * Update ERC-20 to handle `Option` returns * Change `get` and `key` to use `Borrow`-ed values * Add `Debug` and `Default` implementations * Proper spelling * Change `insert` to only accept borrowed K,V pairs * Update ERC-20 example accordingly * Make more explicit what each `key` is referring to * Try using a `RefCell` instead of passing `Key` around * Try using `UnsafeCell` instead * Revert "Try using a `RefCell` instead of passing `Key` around" This reverts commit cede0330f1f60645f19a825801a806fe39d2b0be. Using `RefCell`/`UnsafeCell` doesn't reduce the contract size more than what we have now, and it introduced `unsafe` code. We believe the limiting factor here is the `Key` type definition anyways. * Clean up some of the documentation * adjust the Mapping type for the new SpreadAllocate trait * adjust ERC-20 example for changes in Mapping type * remove commented out code * add doc comment to new_init * make it possible to use references in more cases with Mapping * use references in more cases for ERC-20 example contract * remove unnecessary references in Mapping methods * refactor/improve pull_packed_root_opt utility method slightly * fix ERC-20 example contract The problem with *self.total_supply is that it may implicitly read from storage in case it has not yet read a value from storage whereas Lazy::set just writes the value to the Lazy instance. Co-authored-by: Hernando Castano Co-authored-by: Hernando Castano --- crates/storage/src/collections/mapping/mod.rs | 78 +++++++++------- crates/storage/src/traits/optspec.rs | 24 ++--- examples/erc20/lib.rs | 92 ++++++++++++++----- 3 files changed, 125 insertions(+), 69 deletions(-) diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/collections/mapping/mod.rs index 46e94045774..f5b9fb8aab7 100644 --- a/crates/storage/src/collections/mapping/mod.rs +++ b/crates/storage/src/collections/mapping/mod.rs @@ -14,21 +14,20 @@ //! A simple mapping to contract storage. //! -//! This mapping doesn't actually "own" any data. Instead it is just a simple wrapper around the -//! contract storage facilities. +//! # Note +//! +//! This mapping doesn't actually "own" any data. +//! Instead it is just a simple wrapper around the contract storage facilities. use crate::traits::{ - clear_spread_root, pull_packed_root_opt, - pull_spread_root, push_packed_root, - push_spread_root, ExtKeyPtr, KeyPtr, PackedLayout, + SpreadAllocate, SpreadLayout, }; - use core::marker::PhantomData; use ink_env::hash::{ @@ -42,7 +41,7 @@ use ink_primitives::Key; #[derive(Default)] pub struct Mapping { offset_key: Key, - _marker: PhantomData<(K, V)>, + _marker: PhantomData (K, V)>, } impl core::fmt::Debug for Mapping { @@ -53,29 +52,27 @@ impl core::fmt::Debug for Mapping { } } -impl Mapping -where - K: PackedLayout, - V: PackedLayout, -{ +impl Mapping { /// Creates a new empty `Mapping`. - /// - /// TODO [#961]: Ideally we improve how this is initialized by extending the - /// `SpreadLayout`/`PackedLayout` traits for non-caching data structures. - pub fn new(offset_key: Key) -> Self { + fn new(offset_key: Key) -> Self { Self { offset_key, _marker: Default::default(), } } +} +impl Mapping +where + K: PackedLayout, + V: PackedLayout, +{ /// Insert the given `value` to the contract storage. - pub fn insert(&mut self, key: &Q, value: &R) + #[inline] + pub fn insert(&mut self, key: Q, value: &R) where - K: core::borrow::Borrow, - Q: scale::Encode, - V: core::borrow::Borrow, - R: PackedLayout, + Q: scale::EncodeLike, + R: scale::EncodeLike + PackedLayout, { push_packed_root(value, &self.storage_key(key)); } @@ -83,22 +80,21 @@ where /// Get the `value` at `key` from the contract storage. /// /// Returns `None` if no `value` exists at the given `key`. - pub fn get(&self, key: &Q) -> Option + #[inline] + pub fn get(&self, key: Q) -> Option where - K: core::borrow::Borrow, - Q: scale::Encode, + Q: scale::EncodeLike, { pull_packed_root_opt(&self.storage_key(key)) } /// Returns a `Key` pointer used internally by the storage API. /// - /// This key is a combination of the `Mapping`'s internal `offset_key` and the user provided - /// `key`. - fn storage_key(&self, key: &Q) -> Key + /// This key is a combination of the `Mapping`'s internal `offset_key` + /// and the user provided `key`. + fn storage_key(&self, key: Q) -> Key where - K: core::borrow::Borrow, - Q: scale::Encode, + Q: scale::EncodeLike, { let encodedable_key = (&self.offset_key, key); let mut output = ::Type::default(); @@ -113,20 +109,32 @@ impl SpreadLayout for Mapping { #[inline] fn pull_spread(ptr: &mut KeyPtr) -> Self { - let root_key = ExtKeyPtr::next_for::(ptr); - pull_spread_root::(root_key) + // Note: There is no need to pull anything from the storage for the + // mapping type since it initializes itself entirely by the + // given key pointer. + Self::new(*ExtKeyPtr::next_for::(ptr)) } #[inline] fn push_spread(&self, ptr: &mut KeyPtr) { - let root_key = ExtKeyPtr::next_for::(ptr); - push_spread_root::(self, root_key); + // Note: The mapping type does not store any state in its associated + // storage region, therefore only the pointer has to be incremented. + ptr.advance_by(Self::FOOTPRINT); } #[inline] fn clear_spread(&self, ptr: &mut KeyPtr) { - let root_key = ExtKeyPtr::next_for::(ptr); - clear_spread_root::(self, root_key); + // Note: The mapping type is not aware of its elements, therefore + // it is not possible to clean up after itself. + ptr.advance_by(Self::FOOTPRINT); + } +} + +impl SpreadAllocate for Mapping { + #[inline] + fn allocate_spread(ptr: &mut KeyPtr) -> Self { + // Note: The mapping type initializes itself entirely by the key pointer. + Self::new(*ExtKeyPtr::next_for::(ptr)) } } diff --git a/crates/storage/src/traits/optspec.rs b/crates/storage/src/traits/optspec.rs index 3e28a9d86ce..6d8697f057a 100644 --- a/crates/storage/src/traits/optspec.rs +++ b/crates/storage/src/traits/optspec.rs @@ -102,17 +102,19 @@ pub fn pull_packed_root_opt(root_key: &Key) -> Option where T: PackedLayout, { - match ink_env::get_contract_storage::(root_key) - .expect("decoding does not match expected type") - { - Some(mut value) => { - // In case the contract storage is occupied we handle - // the Option as if it was a T. + ink_env::get_contract_storage::(root_key) + .unwrap_or_else(|error| { + panic!( + "failed to pull packed from root key {}: {:?}", + root_key, error + ) + }) + .map(|mut value| { + // In case the contract storage is occupied at the root key + // we handle the Option as if it was a T. ::pull_packed(&mut value, root_key); - Some(value) - } - None => None, - } + value + }) } pub fn push_packed_root_opt(entity: Option<&T>, root_key: &Key) @@ -128,7 +130,7 @@ where super::push_packed_root(value, root_key) } None => { - // Clear the associated storage cell. + // Clear the associated storage cell since the entity is `None`. ink_env::clear_contract_storage(root_key); } } diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index 76abe2fda7a..4f15dd6aac0 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -4,9 +4,14 @@ use ink_lang as ink; #[ink::contract] mod erc20 { + use ink_primitives::{ + Key, + KeyPtr, + }; use ink_storage::{ collections::mapping::Mapping, lazy::Lazy, + traits::SpreadAllocate, }; /// A simple ERC-20 contract. @@ -55,24 +60,37 @@ mod erc20 { /// The ERC-20 result type. pub type Result = core::result::Result; + impl SpreadAllocate for Erc20 { + fn allocate_spread(ptr: &mut KeyPtr) -> Self { + Self { + total_supply: SpreadAllocate::allocate_spread(ptr), + balances: SpreadAllocate::allocate_spread(ptr), + allowances: SpreadAllocate::allocate_spread(ptr), + } + } + } + impl Erc20 { /// Creates a new ERC-20 contract with the specified initial supply. #[ink(constructor)] pub fn new(initial_supply: Balance) -> Self { + let root_key = Key::from([0x00; 32]); + let mut key_ptr = KeyPtr::from(root_key); + let mut instance = Self::allocate_spread(&mut key_ptr); + instance.new_init(initial_supply); + instance + } + + /// Default initializes the ERC-20 contract with the specified initial supply. + fn new_init(&mut self, initial_supply: Balance) { let caller = Self::env().caller(); - let mut balances = Mapping::new([1u8; 32].into()); - balances.insert(&caller, &initial_supply); - let instance = Self { - total_supply: Lazy::new(initial_supply), - balances, - allowances: Mapping::new([1u8; 32].into()), - }; + self.balances.insert(&caller, &initial_supply); + Lazy::set(&mut self.total_supply, initial_supply); Self::env().emit_event(Transfer { from: None, to: Some(caller), value: initial_supply, }); - instance } /// Returns the total token supply. @@ -86,7 +104,20 @@ mod erc20 { /// Returns `0` if the account is non-existent. #[ink(message)] pub fn balance_of(&self, owner: AccountId) -> Balance { - self.balances.get(&owner).unwrap_or_default() // .copied().unwrap_or(0) + self.balance_of_impl(&owner) + } + + /// Returns the account balance for the specified `owner`. + /// + /// Returns `0` if the account is non-existent. + /// + /// # Note + /// + /// Prefer to call this method over `balance_of` since this + /// works using references which are more efficient in Wasm. + #[inline] + fn balance_of_impl(&self, owner: &AccountId) -> Balance { + self.balances.get(owner).unwrap_or_default() } /// Returns the amount which `spender` is still allowed to withdraw from `owner`. @@ -94,7 +125,20 @@ mod erc20 { /// Returns `0` if no allowance has been set `0`. #[ink(message)] pub fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance { - self.allowances.get(&(owner, spender)).unwrap_or_default() //.copied().unwrap_or(0) + self.allowance_impl(&owner, &spender) + } + + /// Returns the amount which `spender` is still allowed to withdraw from `owner`. + /// + /// Returns `0` if no allowance has been set `0`. + /// + /// # Note + /// + /// Prefer to call this method over `allowance` since this + /// works using references which are more efficient in Wasm. + #[inline] + fn allowance_impl(&self, owner: &AccountId, spender: &AccountId) -> Balance { + self.allowances.get((owner, spender)).unwrap_or_default() } /// Transfers `value` amount of tokens from the caller's account to account `to`. @@ -108,7 +152,7 @@ mod erc20 { #[ink(message)] pub fn transfer(&mut self, to: AccountId, value: Balance) -> Result<()> { let from = self.env().caller(); - self.transfer_from_to(from, to, value) + self.transfer_from_to(&from, &to, value) } /// Allows `spender` to withdraw from the caller's account multiple times, up to @@ -120,7 +164,7 @@ mod erc20 { #[ink(message)] pub fn approve(&mut self, spender: AccountId, value: Balance) -> Result<()> { let owner = self.env().caller(); - self.allowances.insert(&(owner, spender), &value); + self.allowances.insert((&owner, &spender), &value); self.env().emit_event(Approval { owner, spender, @@ -151,12 +195,13 @@ mod erc20 { value: Balance, ) -> Result<()> { let caller = self.env().caller(); - let allowance = self.allowance(from, caller); + let allowance = self.allowance_impl(&from, &caller); if allowance < value { return Err(Error::InsufficientAllowance) } - self.transfer_from_to(from, to, value)?; - self.allowances.insert(&(from, caller), &(allowance - value)); + self.transfer_from_to(&from, &to, value)?; + self.allowances + .insert((&from, &caller), &(allowance - value)); Ok(()) } @@ -170,20 +215,21 @@ mod erc20 { /// the caller's account balance. fn transfer_from_to( &mut self, - from: AccountId, - to: AccountId, + from: &AccountId, + to: &AccountId, value: Balance, ) -> Result<()> { - let from_balance = self.balance_of(from); + let from_balance = self.balance_of_impl(from); if from_balance < value { return Err(Error::InsufficientBalance) } - self.balances.insert(&from, &(from_balance - value)); - let to_balance = self.balance_of(to); - self.balances.insert(&to, &(to_balance + value)); + + self.balances.insert(from, &(from_balance - value)); + let to_balance = self.balance_of_impl(to); + self.balances.insert(to, &(to_balance + value)); self.env().emit_event(Transfer { - from: Some(from), - to: Some(to), + from: Some(*from), + to: Some(*to), value, }); Ok(()) From c23e679a5791c66a83bcc9e57bb1ac2e5f503831 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Tue, 9 Nov 2021 17:31:53 +0100 Subject: [PATCH 29/36] Use new `initialize_contract()` function --- examples/erc20/lib.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index 4f15dd6aac0..a8a1c9e538d 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -74,11 +74,7 @@ mod erc20 { /// Creates a new ERC-20 contract with the specified initial supply. #[ink(constructor)] pub fn new(initial_supply: Balance) -> Self { - let root_key = Key::from([0x00; 32]); - let mut key_ptr = KeyPtr::from(root_key); - let mut instance = Self::allocate_spread(&mut key_ptr); - instance.new_init(initial_supply); - instance + ink_lang::codegen::initialize_contract(|contract| Self::new_init(contract, initial_supply)) } /// Default initializes the ERC-20 contract with the specified initial supply. From 334a9daaa192417c30cb774f6a0e91bd1b98ec34 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Tue, 9 Nov 2021 17:50:20 +0100 Subject: [PATCH 30/36] Derive `SpreadAllocate` for `ink(storage)` structs --- crates/lang/codegen/src/generator/storage.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/lang/codegen/src/generator/storage.rs b/crates/lang/codegen/src/generator/storage.rs index d7f68e5f0f4..5ee3dc10811 100644 --- a/crates/lang/codegen/src/generator/storage.rs +++ b/crates/lang/codegen/src/generator/storage.rs @@ -97,6 +97,7 @@ impl Storage<'_> { derive(::ink_storage::traits::StorageLayout) )] #[derive(::ink_storage::traits::SpreadLayout)] + #[derive(::ink_storage::traits::SpreadAllocate)] #[cfg_attr(test, derive(::core::fmt::Debug))] pub struct #ident { #( #fields ),* From 408bf81d8fe6df1187d342e68b9b9e1ac905a773 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Tue, 9 Nov 2021 17:50:42 +0100 Subject: [PATCH 31/36] Stop manually implementing SpreadAllocate for ERC-20 --- examples/erc20/lib.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index a8a1c9e538d..1cf1bdc9f39 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -4,14 +4,9 @@ use ink_lang as ink; #[ink::contract] mod erc20 { - use ink_primitives::{ - Key, - KeyPtr, - }; use ink_storage::{ collections::mapping::Mapping, lazy::Lazy, - traits::SpreadAllocate, }; /// A simple ERC-20 contract. @@ -60,16 +55,6 @@ mod erc20 { /// The ERC-20 result type. pub type Result = core::result::Result; - impl SpreadAllocate for Erc20 { - fn allocate_spread(ptr: &mut KeyPtr) -> Self { - Self { - total_supply: SpreadAllocate::allocate_spread(ptr), - balances: SpreadAllocate::allocate_spread(ptr), - allowances: SpreadAllocate::allocate_spread(ptr), - } - } - } - impl Erc20 { /// Creates a new ERC-20 contract with the specified initial supply. #[ink(constructor)] From b625a0a6b94d8c4be44744efb1ce4d0053ae2d41 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Tue, 9 Nov 2021 18:08:59 +0100 Subject: [PATCH 32/36] Stop implementing `SpreadAllocate` in the storage codegen --- crates/lang/codegen/src/generator/storage.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/lang/codegen/src/generator/storage.rs b/crates/lang/codegen/src/generator/storage.rs index 5ee3dc10811..d7f68e5f0f4 100644 --- a/crates/lang/codegen/src/generator/storage.rs +++ b/crates/lang/codegen/src/generator/storage.rs @@ -97,7 +97,6 @@ impl Storage<'_> { derive(::ink_storage::traits::StorageLayout) )] #[derive(::ink_storage::traits::SpreadLayout)] - #[derive(::ink_storage::traits::SpreadAllocate)] #[cfg_attr(test, derive(::core::fmt::Debug))] pub struct #ident { #( #fields ),* From 2184cd66e68441780933717365e8e607bd4b97bb Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Tue, 9 Nov 2021 18:09:36 +0100 Subject: [PATCH 33/36] Derive `SpreadAllocate` manually for ERC-20 --- examples/erc20/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index 1cf1bdc9f39..607b5d947ce 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -7,10 +7,12 @@ mod erc20 { use ink_storage::{ collections::mapping::Mapping, lazy::Lazy, + traits::{SpreadAllocate} }; /// A simple ERC-20 contract. #[ink(storage)] + #[derive(SpreadAllocate)] pub struct Erc20 { /// Total token supply. total_supply: Lazy, From 1854dcfdc38531743bbb2182310cd52a816f5742 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Tue, 9 Nov 2021 18:24:34 +0100 Subject: [PATCH 34/36] RustFmt example --- examples/erc20/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index 607b5d947ce..fe333bed85f 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -7,7 +7,7 @@ mod erc20 { use ink_storage::{ collections::mapping::Mapping, lazy::Lazy, - traits::{SpreadAllocate} + traits::SpreadAllocate, }; /// A simple ERC-20 contract. @@ -61,7 +61,9 @@ mod erc20 { /// Creates a new ERC-20 contract with the specified initial supply. #[ink(constructor)] pub fn new(initial_supply: Balance) -> Self { - ink_lang::codegen::initialize_contract(|contract| Self::new_init(contract, initial_supply)) + ink_lang::codegen::initialize_contract(|contract| { + Self::new_init(contract, initial_supply) + }) } /// Default initializes the ERC-20 contract with the specified initial supply. From c4a951ca9b70f9f5caf4cac5ea8971db02180a80 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Fri, 12 Nov 2021 11:29:53 +0100 Subject: [PATCH 35/36] Move `Mapping` from `collections` to `lazy` --- crates/storage/src/collections/mod.rs | 2 -- .../src/{collections/mapping/mod.rs => lazy/mapping.rs} | 0 crates/storage/src/lazy/mod.rs | 2 ++ examples/erc20/lib.rs | 6 ++++-- 4 files changed, 6 insertions(+), 4 deletions(-) rename crates/storage/src/{collections/mapping/mod.rs => lazy/mapping.rs} (100%) diff --git a/crates/storage/src/collections/mod.rs b/crates/storage/src/collections/mod.rs index 1875ce41b00..07ade806f2e 100644 --- a/crates/storage/src/collections/mod.rs +++ b/crates/storage/src/collections/mod.rs @@ -22,7 +22,6 @@ pub mod binary_heap; pub mod bitstash; pub mod bitvec; pub mod hashmap; -pub mod mapping; pub mod smallvec; pub mod stash; pub mod vec; @@ -33,7 +32,6 @@ pub use self::{ bitstash::BitStash, bitvec::Bitvec, hashmap::HashMap, - mapping::Mapping, stash::Stash, vec::Vec, }; diff --git a/crates/storage/src/collections/mapping/mod.rs b/crates/storage/src/lazy/mapping.rs similarity index 100% rename from crates/storage/src/collections/mapping/mod.rs rename to crates/storage/src/lazy/mapping.rs diff --git a/crates/storage/src/lazy/mod.rs b/crates/storage/src/lazy/mod.rs index c9726a57164..b2076ccb251 100644 --- a/crates/storage/src/lazy/mod.rs +++ b/crates/storage/src/lazy/mod.rs @@ -25,6 +25,7 @@ //! extra care has to be taken when operating directly on them. pub mod lazy_hmap; +pub mod mapping; mod cache_cell; mod entry; @@ -46,6 +47,7 @@ pub use self::{ lazy_cell::LazyCell, lazy_hmap::LazyHashMap, lazy_imap::LazyIndexMap, + mapping::Mapping, }; use crate::traits::{ KeyPtr, diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index fe333bed85f..09c2851b882 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -5,8 +5,10 @@ use ink_lang as ink; #[ink::contract] mod erc20 { use ink_storage::{ - collections::mapping::Mapping, - lazy::Lazy, + lazy::{ + Lazy, + Mapping, + }, traits::SpreadAllocate, }; From 5e5a918109263680fb4ab625cc2d533c9d883ef7 Mon Sep 17 00:00:00 2001 From: Hernando Castano Date: Fri, 12 Nov 2021 11:33:02 +0100 Subject: [PATCH 36/36] Remove extra `0` in docs --- examples/erc20/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/erc20/lib.rs b/examples/erc20/lib.rs index 09c2851b882..5fca7d52be9 100644 --- a/examples/erc20/lib.rs +++ b/examples/erc20/lib.rs @@ -109,7 +109,7 @@ mod erc20 { /// Returns the amount which `spender` is still allowed to withdraw from `owner`. /// - /// Returns `0` if no allowance has been set `0`. + /// Returns `0` if no allowance has been set. #[ink(message)] pub fn allowance(&self, owner: AccountId, spender: AccountId) -> Balance { self.allowance_impl(&owner, &spender) @@ -117,7 +117,7 @@ mod erc20 { /// Returns the amount which `spender` is still allowed to withdraw from `owner`. /// - /// Returns `0` if no allowance has been set `0`. + /// Returns `0` if no allowance has been set. /// /// # Note ///