From eb725b294a2d5ad0655c07607ea11a0a4c4ed26c Mon Sep 17 00:00:00 2001 From: Oleg Bondar Date: Tue, 25 Feb 2025 22:12:53 +0000 Subject: [PATCH 1/2] docs: Add PrimaryKey serialization example for a custom structures. --- src/pages/cw-storage-plus/containers/map.mdx | 137 +++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/src/pages/cw-storage-plus/containers/map.mdx b/src/pages/cw-storage-plus/containers/map.mdx index 131e91a2..ce97a138 100644 --- a/src/pages/cw-storage-plus/containers/map.mdx +++ b/src/pages/cw-storage-plus/containers/map.mdx @@ -21,6 +21,7 @@ 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](#advanced) section below for details. ## Values @@ -161,3 +162,139 @@ we lock the first component to `"alice"`, entries are ordered lexicographically component. + +## 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 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 { + 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 { + 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 = 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) -> StdResult { + 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 = 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 + + + 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. From c9949e31ecca5560cc34a92dc85bc5fc33b2feee Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 27 Mar 2025 10:35:15 +0000 Subject: [PATCH 2/2] [autofix.ci] apply automated fixes --- src/pages/cw-storage-plus/containers/map.mdx | 32 +++++++++----------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/pages/cw-storage-plus/containers/map.mdx b/src/pages/cw-storage-plus/containers/map.mdx index ce97a138..86444c9f 100644 --- a/src/pages/cw-storage-plus/containers/map.mdx +++ b/src/pages/cw-storage-plus/containers/map.mdx @@ -20,8 +20,8 @@ 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](#advanced) section below for details. + `serde::Deserialize`. Key encoding is handled by the `PrimaryKey` trait. Please see the + [Advanced](#advanced) section below for details. ## Values @@ -168,11 +168,11 @@ component. ### 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. +[`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 is a widely used enum that represents either a CW20 token or a Native token. +Let's imagine that we would like to use `Denom` as a key in a map like `Map`. Denom +is a widely used enum that represents either a CW20 token or a Native token. Here is a complete implementation. @@ -277,8 +277,8 @@ mod test { 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. +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 @@ -287,14 +287,12 @@ two types on a raw bytes level. #### 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. +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. +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.