From 377368686bc9f619205b32b1378d4fd225747011 Mon Sep 17 00:00:00 2001 From: Tomasz Kurcz Date: Wed, 8 Jan 2025 17:15:43 +0100 Subject: [PATCH 1/3] storey: guide to implementing key types --- src/pages/storey/containers/map/key-impl.mdx | 246 +++++++++++++++++++ 1 file changed, 246 insertions(+) diff --git a/src/pages/storey/containers/map/key-impl.mdx b/src/pages/storey/containers/map/key-impl.mdx index e3fc2b54..a7cafe46 100644 --- a/src/pages/storey/containers/map/key-impl.mdx +++ b/src/pages/storey/containers/map/key-impl.mdx @@ -5,3 +5,249 @@ tags: ["storey", "containers"] import { Callout } from "nextra/components"; # Implementing key types + +In this section, we will implement a custom key type for use with [maps]. + +Let's say we have a `Denom` enum to represent different kinds of tokens: +- native tokens +- [CW20] tokens + +```rust +enum Denom { + Native(String), + CW20(String), +} +``` + +## The Key trait + +We can implement the `Key` trait for this enum to make it usable as a key in a map: + +```rust template="storage" {1, 8-14} +use storey::containers::map::{key::DynamicKey, Key}; + +enum Denom { + Native(String), + CW20(String), +} + +impl Key for Denom { + type Kind = DynamicKey; + + fn encode(&self) -> Vec { + todo!() + } +} +``` + +The [`Kind`] associated type is used to signal to the framework whether the key is dynamically sized or not. In this case, we use [`DynamicKey`] because the key size is not fixed. +If it was always exactly 8 bytes, we could use [`FixedSizeKey<8>`] instead. + + +Why does this matter? The framework uses this information to determine how to encode the key. If there are more keys following this one (e.g. in a multi-level map), the framework needs to know how to tell where this one ends during decoding. + +For a dynamically sized key, the framework will length-prefix it when necessary. For a fixed-size key, it will use the static information you provide with `FixedSizeKey` to figure out how many bytes to eat - a more performant solution. + + +The [`encode`] method is used to serialize the key into a byte vector. Let's implement it now! + +```rust template="storage" {12-21} +use storey::containers::map::{key::DynamicKey, Key}; + +enum Denom { + Native(String), + CW20(String), +} + +impl Key for Denom { + type Kind = DynamicKey; + + fn encode(&self) -> Vec { + let (discriminant, data) = match self { + Denom::Native(data) => (0, data), + Denom::CW20(data) => (1, data), + }; + + let mut result = Vec::with_capacity(1 + data.len()); + result.push(discriminant); + result.extend_from_slice(data.as_bytes()); + + result + } +} +``` + +The code should be pretty self-explanatory. We use a simple encoding scheme where we write a single byte discriminant followed by the actual data. The discriminant is how we tell a native token from a CW20 token. + +One little improvement we can go for is to avoid hardcoding the discriminant. We'll want to reuse these values in the decoding logic, so let's define them as constants: + +```rust template="storage" {8-11, 18-19} +use storey::containers::map::{key::DynamicKey, Key}; + +enum Denom { + Native(String), + CW20(String), +} + +impl Denom { + const NATIVE_DISCRIMINANT: u8 = 0; + const CW20_DISCRIMINANT: u8 = 1; +} + +impl Key for Denom { + type Kind = DynamicKey; + + fn encode(&self) -> Vec { + let (discriminant, data) = match self { + Denom::Native(data) => (Self::NATIVE_DISCRIMINANT, data), + Denom::CW20(data) => (Self::CW20_DISCRIMINANT, data), + }; + + let mut result = Vec::with_capacity(1 + data.len()); + result.push(discriminant); + result.extend_from_slice(data.as_bytes()); + + result + } +} +``` + +Alright. The `Key` trait allows us to access the data in a map using our custom key type. We still need a way to decode the key back into the enum. This is used for example in iteration. + +## The OwnedKey trait + +Let's now implement the [`OwnedKey`] trait. + +```rust template="storage" {30-43} +use storey::containers::map::{key::DynamicKey, Key, OwnedKey}; + +enum Denom { + Native(String), + CW20(String), +} + +impl Denom { + const NATIVE_DISCRIMINANT: u8 = 0; + const CW20_DISCRIMINANT: u8 = 1; +} + +impl Key for Denom { + type Kind = DynamicKey; + + fn encode(&self) -> Vec { + let (discriminant, data) = match self { + Denom::Native(data) => (Self::NATIVE_DISCRIMINANT, data), + Denom::CW20(data) => (Self::CW20_DISCRIMINANT, data), + }; + + let mut result = Vec::with_capacity(1 + data.len()); + result.push(discriminant); + result.extend_from_slice(data.as_bytes()); + + result + } +} + +impl OwnedKey for Denom { + type Error = (); + + fn from_bytes(bytes: &[u8]) -> Result { + let discriminant = bytes[0]; + let data = String::from_utf8(bytes[1..].to_vec()).map_err(|_| ())?; + + match discriminant { + Self::NATIVE_DISCRIMINANT => Ok(Self::Native(data)), + Self::CW20_DISCRIMINANT => Ok(Self::CW20(data)), + _ => Err(()), + } + } +} +``` + +The [`from_bytes`] method should return an instance of the key type or an error if the data is invalid. Here it does the following: +- read the discriminant byte +- read the data bytes as a UTF-8 string, erroring out if it's not valid +- match the discriminant to the enum variant, or error if invalid +- return the deserialized key + + +What we have is functional. There's one last improvement you could make here - a proper error type. We used `()` as a placeholder, but in production code it's good practice to define an enum. In this case, the enum could hold variants +like `InvalidDiscriminant` and `InvalidUTF8`. + + +## Using the thing + +Now that we have our key type implemented, we can use it in a map: + +```rust template="storage" {1-3, 49-64} +use cw_storey::CwStorage; +use cw_storey::containers::{Item, Map}; +use storey::containers::IterableAccessor; +use storey::containers::map::{key::DynamicKey, Key, OwnedKey}; + +#[derive(Debug, PartialEq)] +enum Denom { + Native(String), + CW20(String), +} + +impl Denom { + const NATIVE_DISCRIMINANT: u8 = 0; + const CW20_DISCRIMINANT: u8 = 1; +} + +impl Key for Denom { + type Kind = DynamicKey; + + fn encode(&self) -> Vec { + let (discriminant, data) = match self { + Denom::Native(data) => (Self::NATIVE_DISCRIMINANT, data), + Denom::CW20(data) => (Self::CW20_DISCRIMINANT, data), + }; + + let mut result = Vec::with_capacity(1 + data.len()); + result.push(discriminant); + result.extend_from_slice(data.as_bytes()); + + result + } +} + +impl OwnedKey for Denom { + type Error = (); + + fn from_bytes(bytes: &[u8]) -> Result { + let discriminant = bytes[0]; + let data = String::from_utf8(bytes[1..].to_vec()).map_err(|_| ())?; + + match discriminant { + Self::NATIVE_DISCRIMINANT => Ok(Self::Native(data)), + Self::CW20_DISCRIMINANT => Ok(Self::CW20(data)), + _ => Err(()), + } + } +} + +const MAP_IX: u8 = 1; + +let map: Map> = Map::new(MAP_IX); +let mut cw_storage = CwStorage(&mut storage); +let mut access = map.access(&mut cw_storage); + +access.entry_mut(&Denom::Native("USDT".into())).set(&1000).unwrap(); +access.entry_mut(&Denom::CW20("some_addr_3824792".into())).set(&2000).unwrap(); + +assert_eq!(access.entry(&Denom::Native("USDT".into())).get().unwrap(), Some(1000)); +assert_eq!(access.entry(&Denom::CW20("some_addr_3824792".into())).get().unwrap(), Some(2000)); +``` + +VoilĂ ! It works just like it would with any other key. + +[maps]: /docs/storey/containers/map +[CW20]: /docs/getting-started/cw20 +[`Kind`]: https://docs.rs/storey/latest/storey/containers/map/key/trait.Key.html#associatedtype.Kind +[`DynamicKey`]: https://docs.rs/storey/latest/storey/containers/map/key/struct.DynamicKey.html +[`FixedSizeKey<8>`]: https://docs.rs/storey/latest/storey/containers/map/key/struct.FixedSizeKey.html +[`encode`]: https://docs.rs/storey/latest/storey/containers/map/key/trait.Key.html#tymethod.encode +[`OwnedKey`]: https://docs.rs/storey/latest/storey/containers/map/key/trait.OwnedKey.html +[`from_bytes`]: https://docs.rs/storey/latest/storey/containers/map/key/trait.OwnedKey.html#tymethod.from_bytes From 1c2b3d730d8eb82a849716544cedf3019dc996b8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:19:25 +0000 Subject: [PATCH 2/3] [autofix.ci] apply automated fixes --- src/pages/storey/containers/map/key-impl.mdx | 36 ++++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/src/pages/storey/containers/map/key-impl.mdx b/src/pages/storey/containers/map/key-impl.mdx index a7cafe46..1c51847c 100644 --- a/src/pages/storey/containers/map/key-impl.mdx +++ b/src/pages/storey/containers/map/key-impl.mdx @@ -9,6 +9,7 @@ import { Callout } from "nextra/components"; In this section, we will implement a custom key type for use with [maps]. Let's say we have a `Denom` enum to represent different kinds of tokens: + - native tokens - [CW20] tokens @@ -40,13 +41,17 @@ impl Key for Denom { } ``` -The [`Kind`] associated type is used to signal to the framework whether the key is dynamically sized or not. In this case, we use [`DynamicKey`] because the key size is not fixed. -If it was always exactly 8 bytes, we could use [`FixedSizeKey<8>`] instead. +The [`Kind`] associated type is used to signal to the framework whether the key is dynamically sized +or not. In this case, we use [`DynamicKey`] because the key size is not fixed. If it was always +exactly 8 bytes, we could use [`FixedSizeKey<8>`] instead. Why does this matter? The framework uses this information to determine how to encode the key. If there are more keys following this one (e.g. in a multi-level map), the framework needs to know how to tell where this one ends during decoding. -For a dynamically sized key, the framework will length-prefix it when necessary. For a fixed-size key, it will use the static information you provide with `FixedSizeKey` to figure out how many bytes to eat - a more performant solution. +For a dynamically sized key, the framework will length-prefix it when necessary. For a fixed-size +key, it will use the static information you provide with `FixedSizeKey` to figure out how many +bytes to eat - a more performant solution. + The [`encode`] method is used to serialize the key into a byte vector. Let's implement it now! @@ -77,9 +82,12 @@ impl Key for Denom { } ``` -The code should be pretty self-explanatory. We use a simple encoding scheme where we write a single byte discriminant followed by the actual data. The discriminant is how we tell a native token from a CW20 token. +The code should be pretty self-explanatory. We use a simple encoding scheme where we write a single +byte discriminant followed by the actual data. The discriminant is how we tell a native token from a +CW20 token. -One little improvement we can go for is to avoid hardcoding the discriminant. We'll want to reuse these values in the decoding logic, so let's define them as constants: +One little improvement we can go for is to avoid hardcoding the discriminant. We'll want to reuse +these values in the decoding logic, so let's define them as constants: ```rust template="storage" {8-11, 18-19} use storey::containers::map::{key::DynamicKey, Key}; @@ -112,7 +120,8 @@ impl Key for Denom { } ``` -Alright. The `Key` trait allows us to access the data in a map using our custom key type. We still need a way to decode the key back into the enum. This is used for example in iteration. +Alright. The `Key` trait allows us to access the data in a map using our custom key type. We still +need a way to decode the key back into the enum. This is used for example in iteration. ## The OwnedKey trait @@ -164,15 +173,18 @@ impl OwnedKey for Denom { } ``` -The [`from_bytes`] method should return an instance of the key type or an error if the data is invalid. Here it does the following: +The [`from_bytes`] method should return an instance of the key type or an error if the data is +invalid. Here it does the following: + - read the discriminant byte - read the data bytes as a UTF-8 string, erroring out if it's not valid - match the discriminant to the enum variant, or error if invalid - return the deserialized key -What we have is functional. There's one last improvement you could make here - a proper error type. We used `()` as a placeholder, but in production code it's good practice to define an enum. In this case, the enum could hold variants -like `InvalidDiscriminant` and `InvalidUTF8`. + What we have is functional. There's one last improvement you could make here - a proper error + type. We used `()` as a placeholder, but in production code it's good practice to define an enum. + In this case, the enum could hold variants like `InvalidDiscriminant` and `InvalidUTF8`. ## Using the thing @@ -247,7 +259,9 @@ VoilĂ ! It works just like it would with any other key. [CW20]: /docs/getting-started/cw20 [`Kind`]: https://docs.rs/storey/latest/storey/containers/map/key/trait.Key.html#associatedtype.Kind [`DynamicKey`]: https://docs.rs/storey/latest/storey/containers/map/key/struct.DynamicKey.html -[`FixedSizeKey<8>`]: https://docs.rs/storey/latest/storey/containers/map/key/struct.FixedSizeKey.html +[`FixedSizeKey<8>`]: + https://docs.rs/storey/latest/storey/containers/map/key/struct.FixedSizeKey.html [`encode`]: https://docs.rs/storey/latest/storey/containers/map/key/trait.Key.html#tymethod.encode [`OwnedKey`]: https://docs.rs/storey/latest/storey/containers/map/key/trait.OwnedKey.html -[`from_bytes`]: https://docs.rs/storey/latest/storey/containers/map/key/trait.OwnedKey.html#tymethod.from_bytes +[`from_bytes`]: + https://docs.rs/storey/latest/storey/containers/map/key/trait.OwnedKey.html#tymethod.from_bytes From 24fa9c29219ba8a7c893f931d443f9c408b577e0 Mon Sep 17 00:00:00 2001 From: Tomasz Kurcz Date: Thu, 13 Feb 2025 15:23:13 +0100 Subject: [PATCH 3/3] storey: update key-impl guide --- src/pages/storey/containers/map/key-impl.mdx | 34 +++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/pages/storey/containers/map/key-impl.mdx b/src/pages/storey/containers/map/key-impl.mdx index 1c51847c..7585a144 100644 --- a/src/pages/storey/containers/map/key-impl.mdx +++ b/src/pages/storey/containers/map/key-impl.mdx @@ -24,7 +24,8 @@ enum Denom { We can implement the `Key` trait for this enum to make it usable as a key in a map: -```rust template="storage" {1, 8-14} +```rust template="storage" {1-2, 9-15} +use cw_storey::containers::CwKeySet; use storey::containers::map::{key::DynamicKey, Key}; enum Denom { @@ -32,7 +33,7 @@ enum Denom { CW20(String), } -impl Key for Denom { +impl Key for Denom { type Kind = DynamicKey; fn encode(&self) -> Vec { @@ -56,7 +57,8 @@ bytes to eat - a more performant solution. The [`encode`] method is used to serialize the key into a byte vector. Let's implement it now! -```rust template="storage" {12-21} +```rust template="storage" {13-22} +use cw_storey::containers::CwKeySet; use storey::containers::map::{key::DynamicKey, Key}; enum Denom { @@ -64,7 +66,7 @@ enum Denom { CW20(String), } -impl Key for Denom { +impl Key for Denom { type Kind = DynamicKey; fn encode(&self) -> Vec { @@ -89,7 +91,8 @@ CW20 token. One little improvement we can go for is to avoid hardcoding the discriminant. We'll want to reuse these values in the decoding logic, so let's define them as constants: -```rust template="storage" {8-11, 18-19} +```rust template="storage" {9-12, 19-20} +use cw_storey::containers::CwKeySet; use storey::containers::map::{key::DynamicKey, Key}; enum Denom { @@ -102,7 +105,7 @@ impl Denom { const CW20_DISCRIMINANT: u8 = 1; } -impl Key for Denom { +impl Key for Denom { type Kind = DynamicKey; fn encode(&self) -> Vec { @@ -127,7 +130,8 @@ need a way to decode the key back into the enum. This is used for example in ite Let's now implement the [`OwnedKey`] trait. -```rust template="storage" {30-43} +```rust template="storage" {31-44} +use cw_storey::containers::CwKeySet; use storey::containers::map::{key::DynamicKey, Key, OwnedKey}; enum Denom { @@ -140,7 +144,7 @@ impl Denom { const CW20_DISCRIMINANT: u8 = 1; } -impl Key for Denom { +impl Key for Denom { type Kind = DynamicKey; fn encode(&self) -> Vec { @@ -157,7 +161,7 @@ impl Key for Denom { } } -impl OwnedKey for Denom { +impl OwnedKey for Denom { type Error = (); fn from_bytes(bytes: &[u8]) -> Result { @@ -191,9 +195,8 @@ invalid. Here it does the following: Now that we have our key type implemented, we can use it in a map: -```rust template="storage" {1-3, 49-64} -use cw_storey::CwStorage; -use cw_storey::containers::{Item, Map}; +```rust template="storage" showLineNumbers {1-3, 48-57} +use cw_storey::containers::{Item, Map, CwKeySet}; use storey::containers::IterableAccessor; use storey::containers::map::{key::DynamicKey, Key, OwnedKey}; @@ -208,7 +211,7 @@ impl Denom { const CW20_DISCRIMINANT: u8 = 1; } -impl Key for Denom { +impl Key for Denom { type Kind = DynamicKey; fn encode(&self) -> Vec { @@ -225,7 +228,7 @@ impl Key for Denom { } } -impl OwnedKey for Denom { +impl OwnedKey for Denom { type Error = (); fn from_bytes(bytes: &[u8]) -> Result { @@ -243,8 +246,7 @@ impl OwnedKey for Denom { const MAP_IX: u8 = 1; let map: Map> = Map::new(MAP_IX); -let mut cw_storage = CwStorage(&mut storage); -let mut access = map.access(&mut cw_storage); +let mut access = map.access(&mut storage); access.entry_mut(&Denom::Native("USDT".into())).set(&1000).unwrap(); access.entry_mut(&Denom::CW20("some_addr_3824792".into())).set(&2000).unwrap();