Map
A Map
is a key-value store. Unlike the raw storage backend, the keys and values of a map are
typed.
Keys
The key type has to implement the
PrimaryKey
trait.
Most commonly, the key is simply a String
or
Addr
. Other types include
binary strings (Vec<u8>
, [u8; N]
, &[u8]
), numerical types, or even tuples, which can be used
to create composite keys.
Unlike values, keys do not need to implement anything like serde::Serialize
or
serde::Deserialize
. Key encoding is handled by the PrimaryKey
trait. Please see the
Advanced section below for details.
Values
The values, as usual, are serialized as JSON and must implement the
Serialize
and
Deserialize
traits.
Operations
Similar to an Item
, a Map
defines methods like load
, save
, and may_load
. The
difference is that Map
’s methods take a key as an argument.
Most CosmWasm-enabled chains will enable iteration. If they do, it is possible to iterate over the
entries in a map using methods like
keys
or
range
.
More information can be found in the API docs.
Usage examples
Keeping users’ balances
Checking a user’s balance
use cw_storage_plus::Map;
let balances: Map<String, u128> = Map::new("balances");
let alice_balance = balances
.may_load(&storage, "alice".to_string())
.unwrap()
.unwrap_or(0); // if the entry does not exist yet, assume the balance is 0
assert_eq!(alice_balance, 0);
Updating a user’s balance
use cw_storage_plus::Map;
let balances: Map<String, u128> = Map::new("balances");
let mut alice_balance = balances
.may_load(&storage, "alice".to_string())
.unwrap()
.unwrap_or(0); // if the entry does not exist yet, assume the balance is 0
alice_balance += 100;
balances.save(&mut storage, "alice".to_string(), &alice_balance).unwrap();
assert_eq!(balances.load(&storage, "alice".to_string()).unwrap(), 100);
Keeping users’ balances with a composite key
Basic use
use cw_storage_plus::Map;
// The first string is the user's address, the second is the denomination
let balances: Map<(String, String), u128> = Map::new("balances");
let mut alice_balance = balances
.may_load(&storage, ("alice".to_string(), "uusd".to_string()))
.unwrap()
.unwrap_or(0); // if the entry does not exist yet, assume the balance is 0
alice_balance += 100;
balances
.save(&mut storage, ("alice".to_string(), "uusd".to_string()), &alice_balance)
.unwrap();
assert_eq!(
balances
.load(&storage, ("alice".to_string(), "uusd".to_string()))
.unwrap(),
100
);
Iterating over composite keys
use cw_storage_plus::Map;
let balances: Map<(String, String), u128> = Map::new("balances");
balances.save(&mut storage, ("alice".to_string(), "uusd".to_string()), &100).unwrap();
balances.save(&mut storage, ("alice".to_string(), "osmo".to_string()), &200).unwrap();
balances.save(&mut storage, ("bob".to_string(), "uusd".to_string()), &300).unwrap();
let all_balances: Vec<_> = balances
.range(&storage, None, None, Order::Ascending)
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(
all_balances,
vec![
(("bob".to_string(), "uusd".to_string()), 300),
(("alice".to_string(), "osmo".to_string()), 200),
(("alice".to_string(), "uusd".to_string()), 100),
]
);
let alices_balances: Vec<_> = balances
.prefix("alice".to_string())
.range(&storage, None, None, Order::Ascending)
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(alices_balances, vec![("osmo".to_string(), 200), ("uusd".to_string(), 100)]);
As seen here, the order of keys isn’t always lexicographic.
If you need to rely on iteration order in maps with composite keys, here’s how things work: under the hood, every component of a composite key is length-prefixed except for the last one. If you’re only iterating over the last component, you can expect things to be ordered lexicographically. For non-final components, shorter strings will always come before longer ones.
In the example, note how "bob"
(a non-last component) comes before "alice"
. Also note how once
we lock the first component to "alice"
, entries are ordered lexicographically by the second
component.
Advanced
Using custom types as a Key in a Map storage
The current section provides an example of an implementation for
PrimaryKey
(and
KeyDeserialize
) traits.
Let’s imagine that we would like to use Denom
as a key in a map like Map<Denom, Uint64>
. Denom
is a widely used enum that represents either a CW20 token or a Native token.
Here is a complete implementation.
use cosmwasm_std::{Addr, StdError, StdResult};
use cw_storage_plus::{split_first_key, Key, KeyDeserialize, Prefixer, PrimaryKey};
use std::u8;
#[derive(Clone, Debug, PartialEq, Eq)]
enum Denom {
Native(String),
Cw20(Addr),
}
const NATIVE_PREFIX: u8 = 1;
const CW20_PREFIX: u8 = 2;
impl PrimaryKey<'_> for Denom {
type Prefix = u8;
type SubPrefix = ();
type Suffix = String;
type SuperSuffix = Self;
fn key(&self) -> Vec<Key> {
let (prefix, value) = match self {
Denom::Native(name) => (NATIVE_PREFIX, name.as_bytes()),
Denom::Cw20(addr) => (CW20_PREFIX, addr.as_bytes()),
};
vec![Key::Val8([prefix]), Key::Ref(value)]
}
}
impl Prefixer<'_> for Denom {
fn prefix(&self) -> Vec<Key> {
let (prefix, value) = match self {
Denom::Native(name) => (NATIVE_PREFIX.prefix(), name.prefix()),
Denom::Cw20(addr) => (CW20_PREFIX.prefix(), addr.prefix()),
};
let mut result: Vec<Key> = vec![];
result.extend(prefix);
result.extend(value);
result
}
}
impl KeyDeserialize for Denom {
type Output = Self;
const KEY_ELEMS: u16 = 2;
#[inline(always)]
fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
let (prefix, value) = split_first_key(Self::KEY_ELEMS, value.as_ref())?;
let value = value.to_vec();
match u8::from_vec(prefix)? {
NATIVE_PREFIX => Ok(Denom::Native(String::from_vec(value)?)),
CW20_PREFIX => Ok(Denom::Cw20(Addr::from_vec(value)?)),
_ => Err(StdError::generic_err("Invalid prefix")),
}
}
}
#[cfg(test)]
mod test {
use crate::Denom;
use cosmwasm_std::testing::MockStorage;
use cosmwasm_std::{Addr, Uint64};
use cw_storage_plus::Map;
#[test]
fn round_trip_tests() {
let test_data = vec![
Denom::Native("cosmos".to_string()),
Denom::Native("some_long_native_value_with_high_precise".to_string()),
Denom::Cw20(Addr::unchecked("contract1")),
Denom::Cw20(Addr::unchecked(
"cosmos1p7d8mnjttcszv34pk2a5yyug3474mhffasa7tg",
)),
];
for denom in test_data {
verify_map_serde(denom);
}
}
fn verify_map_serde(denom: Denom) {
let mut storage = MockStorage::new();
let map: Map<Denom, Uint64> = Map::new("denom_map");
let mock_value = Uint64::from(123u64);
map.save(&mut storage, denom.clone(), &mock_value).unwrap();
assert!(map.has(&storage, denom.clone()), "key should exist");
let value = map.load(&storage, denom).unwrap();
assert_eq!(value, mock_value, "value should match");
}
}
The idea is to store the Denom in the storage as a composite key with 2 elements:
- The prefix which is either
NATIVE_PREFIX
orCW20_PREFIX
to differentiate between the two types on a raw bytes level. - The value which is either the native token name or the cw20 token address
The example implementation is based on a so-called composite key i.e. with KEY_ELEMS=2.
Choosing between single element key and composite key
The composite key approach in our example was chosen for demonstration purpose rather than a common logic. Because such a prefix with only 2 unique values doesn’t give much profit in terms of index lookup performance vs full-collection lookup. Also, Denom’s type and its value are logically coupled with each other so there is not much need to split them into prefixes and suffixes.
Another example is a User entity with first_name
and last_name
fields where such a separation
for prefixes and suffixes would be natural. In this case, if you define first_name
as a prefix and
last_name
as a suffix you will be able to have indexed search to query all John’s; and have to
fetch all keys to find all Doe’s.