Storage Maps
Another important common collection is the storage map. The type StorageMap<K, V>
from the standard library stores a mapping of keys of type K
to values of type V
using a hashing function, which determines how it places these keys and values into storage slots. This is similar to Rust's HashMap<K, V>
but with a few differences.
Storage maps are useful when you want to look up data not by using an index, as you can with vectors, but by using a key that can be of any type. For example, when building a ledger-based sub-currency smart contract, you could keep track of the balance of each wallet in a storage map in which each key is a wallet’s Address
and the values are each wallet’s balance. Given an Address
, you can retrieve its balance.
Similarly to StorageVec<T>
, StorageMap<K, V>
can only be used in a contract because only contracts are allowed to access persistent storage.
StorageMap<T>
is included in the standard library prelude which means that there is no need to import it manually.
Creating a New Storage Map
To create a new empty storage map, we have to declare the map in a storage
block as follows:
map: StorageMap<Address, u64> = StorageMap {},
Just like any other storage variable, two things are required when declaring a StorageMap
: a type annotation and an initializer. The initializer is just an empty struct of type StorageMap
because StorageMap<K, V>
itself is an empty struct! Everything that is interesting about StorageMap<K, V>
is implemented in its methods.
Storage maps, just like Vec<T>
and StorageVec<T>
, are implemented using generics which means that the StorageMap<K, V>
type provided by the standard library can map keys of any type K
to values of any type V
. In the example above, we’ve told the Sway compiler that the StorageMap<K, V>
in map
will map keys of type Address
to values of type u64
.
Updating a Storage Map
To insert key-value pairs into a storage map, we can use the insert
method, as shown below:
#[storage(write)]
fn insert_into_storage_map() {
let addr1 = Address::from(0x0101010101010101010101010101010101010101010101010101010101010101);
let addr2 = Address::from(0x0202020202020202020202020202020202020202020202020202020202020202);
storage.map.insert(addr1, 42);
storage.map.insert(addr2, 77);
}
Note two details here. First, in order to use insert
, we need to first access the storage map using the storage
keyword. Second, because insert
requires writing into storage, a #[storage(write)]
annotation is required on the ABI function that calls insert
.
Note The storage annotation is also required for any private function defined in the contract that tries to insert into the map.
Note There is no need to add the
mut
keyword when declaring aStorageMap<K, V>
. All storage variables are mutable by default.
Accessing Values in a Storage Map
We can get a value out of the storage map by providing its key
to the get
method, as shown below:
#[storage(read, write)]
fn get_from_storage_map() {
let addr1 = Address::from(0x0101010101010101010101010101010101010101010101010101010101010101);
let addr2 = Address::from(0x0202020202020202020202020202020202020202020202020202020202020202);
storage.map.insert(addr1, 42);
storage.map.insert(addr2, 77);
let value1 = storage.map.get(addr1);
}
Here, value1
will have the value that's associated with the first address, and the result will be 42
. You might expect get
to return an Option<V>
where the return value would be None
if the value does not exist. However, that is not case for StorageMap
. In fact, storage maps have no way of knowing whether insert
has been called with a given key or not as it would be too expensive to keep track of that information. Instead, a default value whose byte-representation is all zeros is returned if get
is called with a key that has no value in the map. Note that each type interprets that default value differently:
- The default value for a
bool
isfalse
. - The default value for a integers is
0
. - The default value for a
b256
is0x0000000000000000000000000000000000000000000000000000000000000000
. - The default value for a
str[n]
is a string ofNull
characters. - The default value for a tuple is a tuple of the default values of its components.
- The default value for a struct is a struct of the default values of its components.
- The default value for an enum is an instance of its first variant containing the default for its associated value.
Storage maps with multiple keys
You might find yourself needing a StorageMap<K1, V1>
where the type V1
is itself another StorageMap<K2, V2>
. This is not allowed in Sway. The right approach is to use a single StorageMap<K, V>
where K
is a tuple (K1, K2)
. For example:
map_two_keys: StorageMap<(b256, bool), b256> = StorageMap {},
Limitations
It is not currently allowed to have a StorageMap<K, V>
as a component of a complex type such as a struct or an enum. For example, the code below is not legal:
Struct Wrapper {
map1: StorageMap<u64, u64>,
map2: StorageMap<u64, u64>,
}
storage {
w: Wrapper
}
...
storage.w.map1.insert(..);