Skip to content
This repository was archived by the owner on Dec 12, 2025. It is now read-only.

Commit cfbcea3

Browse files
authored
docs: Add PrimaryKey serialization example for custom structures (#254)
2 parents c4d0a4e + c9949e3 commit cfbcea3

File tree

1 file changed

+136
-1
lines changed
  • src/pages/cw-storage-plus/containers

1 file changed

+136
-1
lines changed

src/pages/cw-storage-plus/containers/map.mdx

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ to create composite keys.
2020

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

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

163164
</Callout>
165+
166+
## Advanced
167+
168+
### Using custom types as a Key in a Map storage
169+
170+
The current section provides an example of an implementation for
171+
[`PrimaryKey`](https://docs.rs/cw-storage-plus/latest/cw_storage_plus/trait.PrimaryKey.html) (and
172+
`KeyDeserialize`) traits.
173+
174+
Let's imagine that we would like to use `Denom` as a key in a map like `Map<Denom, Uint64>`. Denom
175+
is a widely used enum that represents either a CW20 token or a Native token.
176+
177+
Here is a complete implementation.
178+
179+
```rust
180+
use cosmwasm_std::{Addr, StdError, StdResult};
181+
use cw_storage_plus::{split_first_key, Key, KeyDeserialize, Prefixer, PrimaryKey};
182+
use std::u8;
183+
184+
#[derive(Clone, Debug, PartialEq, Eq)]
185+
enum Denom {
186+
Native(String),
187+
Cw20(Addr),
188+
}
189+
190+
const NATIVE_PREFIX: u8 = 1;
191+
const CW20_PREFIX: u8 = 2;
192+
193+
impl PrimaryKey<'_> for Denom {
194+
type Prefix = u8;
195+
type SubPrefix = ();
196+
type Suffix = String;
197+
type SuperSuffix = Self;
198+
199+
fn key(&self) -> Vec<Key> {
200+
let (prefix, value) = match self {
201+
Denom::Native(name) => (NATIVE_PREFIX, name.as_bytes()),
202+
Denom::Cw20(addr) => (CW20_PREFIX, addr.as_bytes()),
203+
};
204+
vec![Key::Val8([prefix]), Key::Ref(value)]
205+
}
206+
}
207+
208+
impl Prefixer<'_> for Denom {
209+
fn prefix(&self) -> Vec<Key> {
210+
let (prefix, value) = match self {
211+
Denom::Native(name) => (NATIVE_PREFIX.prefix(), name.prefix()),
212+
Denom::Cw20(addr) => (CW20_PREFIX.prefix(), addr.prefix()),
213+
};
214+
215+
let mut result: Vec<Key> = vec![];
216+
result.extend(prefix);
217+
result.extend(value);
218+
result
219+
}
220+
}
221+
222+
impl KeyDeserialize for Denom {
223+
type Output = Self;
224+
225+
const KEY_ELEMS: u16 = 2;
226+
227+
#[inline(always)]
228+
fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
229+
let (prefix, value) = split_first_key(Self::KEY_ELEMS, value.as_ref())?;
230+
let value = value.to_vec();
231+
232+
match u8::from_vec(prefix)? {
233+
NATIVE_PREFIX => Ok(Denom::Native(String::from_vec(value)?)),
234+
CW20_PREFIX => Ok(Denom::Cw20(Addr::from_vec(value)?)),
235+
_ => Err(StdError::generic_err("Invalid prefix")),
236+
}
237+
}
238+
}
239+
240+
#[cfg(test)]
241+
mod test {
242+
use crate::Denom;
243+
use cosmwasm_std::testing::MockStorage;
244+
use cosmwasm_std::{Addr, Uint64};
245+
use cw_storage_plus::Map;
246+
247+
#[test]
248+
fn round_trip_tests() {
249+
let test_data = vec![
250+
Denom::Native("cosmos".to_string()),
251+
Denom::Native("some_long_native_value_with_high_precise".to_string()),
252+
Denom::Cw20(Addr::unchecked("contract1")),
253+
Denom::Cw20(Addr::unchecked(
254+
"cosmos1p7d8mnjttcszv34pk2a5yyug3474mhffasa7tg",
255+
)),
256+
];
257+
258+
for denom in test_data {
259+
verify_map_serde(denom);
260+
}
261+
}
262+
263+
fn verify_map_serde(denom: Denom) {
264+
let mut storage = MockStorage::new();
265+
let map: Map<Denom, Uint64> = Map::new("denom_map");
266+
let mock_value = Uint64::from(123u64);
267+
268+
map.save(&mut storage, denom.clone(), &mock_value).unwrap();
269+
270+
assert!(map.has(&storage, denom.clone()), "key should exist");
271+
272+
let value = map.load(&storage, denom).unwrap();
273+
assert_eq!(value, mock_value, "value should match");
274+
}
275+
}
276+
```
277+
278+
The idea is to store the Denom in the storage as a composite key with 2 elements:
279+
280+
1. The prefix which is either `NATIVE_PREFIX` or `CW20_PREFIX` to differentiate between the two
281+
types on a raw bytes level.
282+
2. The value which is either the native token name or the cw20 token address
283+
284+
<Callout>
285+
The example implementation is based on a so-called composite key i.e. with KEY_ELEMS=2.
286+
</Callout>
287+
288+
#### Choosing between single element key and composite key
289+
290+
The composite key approach in our example was chosen for demonstration purpose rather than a common
291+
logic. Because such a prefix with only 2 unique values doesn't give much profit in terms of index
292+
lookup performance vs full-collection lookup. Also, Denom's type and its value are logically coupled
293+
with each other so there is not much need to split them into prefixes and suffixes.
294+
295+
Another example is a User entity with `first_name` and `last_name` fields where such a separation
296+
for prefixes and suffixes would be natural. In this case, if you define `first_name` as a prefix and
297+
`last_name` as a suffix you will be able to have indexed search to query all John's; and have to
298+
fetch all keys to find all Doe's.

0 commit comments

Comments
 (0)