Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 136 additions & 1 deletion src/pages/cw-storage-plus/containers/map.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ to create composite keys.

<Callout>
Unlike values, keys do **not** need to implement anything like `serde::Serialize` or
`serde::Deserialize`. Key encoding is handled by the `PrimaryKey` trait.
`serde::Deserialize`. Key encoding is handled by the `PrimaryKey` trait. Please see the
[Advanced](#advanced) section below for details.
</Callout>

## Values
Expand Down Expand Up @@ -161,3 +162,137 @@ we lock the first component to `"alice"`, entries are ordered lexicographically
component.

</Callout>

## Advanced

### Using custom types as a Key in a Map storage

The current section provides an example of an implementation for
[`PrimaryKey`](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/trait.PrimaryKey.html) (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.

```rust
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:

1. The prefix which is either `NATIVE_PREFIX` or `CW20_PREFIX` to differentiate between the two
types on a raw bytes level.
2. The value which is either the native token name or the cw20 token address

<Callout>
The example implementation is based on a so-called composite key i.e. with KEY_ELEMS=2.
</Callout>

#### 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.
Loading