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.
In order to use StorageMap<K, V>
, you must first import StorageMap
as follows:
use std::storage::StorageMap;
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 = 0x0101010101010101010101010101010101010101010101010101010101010101;
let addr2 = 0x0202020202020202020202020202020202020202020202020202020202020202;
storage.map.insert(~Address::from(addr1), 42);
storage.map.insert(~Address::from(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 = 0x0101010101010101010101010101010101010101010101010101010101010101;
let addr2 = 0x0202020202020202020202020202020202020202020202020202020202020202;
storage.map.insert(~Address::from(addr1), 42);
storage.map.insert(~Address::from(addr2), 77);
let value1 = storage.map.get(~Address::from(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(..);