abstract_std/objects/entry/
asset_entry.rs

1use std::fmt::Display;
2
3use cosmwasm_std::StdResult;
4use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use crate::{constants::CHAIN_DELIMITER, AbstractError, AbstractResult};
9
10/// An unchecked ANS asset entry. This is a string that is formatted as
11/// `src_chain>[intermediate_chain>]asset_name`
12#[derive(
13    Deserialize, Serialize, Clone, Debug, PartialEq, Eq, JsonSchema, PartialOrd, Ord, Default,
14)]
15pub struct AssetEntry(pub(crate) String);
16
17impl AssetEntry {
18    pub fn new(entry: &str) -> Self {
19        Self(str::to_ascii_lowercase(entry))
20    }
21    pub fn as_str(&self) -> &str {
22        &self.0
23    }
24    pub fn format(&mut self) {
25        self.0 = self.0.to_ascii_lowercase();
26    }
27
28    /// Retrieve the source chain of the asset
29    /// Example: osmosis>juno>crab returns osmosis
30    pub fn src_chain(&self) -> AbstractResult<String> {
31        let mut split = self.0.splitn(2, CHAIN_DELIMITER);
32
33        match split.next() {
34            Some(src_chain) => {
35                if src_chain.is_empty() {
36                    return self.entry_formatting_error();
37                }
38                // Ensure there's at least one more element (asset_name)
39                let maybe_asset_name = split.next();
40                if maybe_asset_name.is_some() && maybe_asset_name != Some("") {
41                    Ok(src_chain.to_string())
42                } else {
43                    self.entry_formatting_error()
44                }
45            }
46            None => self.entry_formatting_error(),
47        }
48    }
49
50    fn entry_formatting_error(&self) -> AbstractResult<String> {
51        Err(AbstractError::EntryFormattingError {
52            actual: self.0.clone(),
53            expected: "src_chain>asset_name".to_string(),
54        })
55    }
56}
57
58impl From<&str> for AssetEntry {
59    fn from(entry: &str) -> Self {
60        Self::new(entry)
61    }
62}
63
64impl From<String> for AssetEntry {
65    fn from(entry: String) -> Self {
66        Self::new(&entry)
67    }
68}
69
70impl From<&String> for AssetEntry {
71    fn from(entry: &String) -> Self {
72        Self::new(entry)
73    }
74}
75
76impl Display for AssetEntry {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        write!(f, "{}", self.0)
79    }
80}
81
82impl PrimaryKey<'_> for AssetEntry {
83    type Prefix = ();
84
85    type SubPrefix = ();
86
87    type Suffix = Self;
88
89    type SuperSuffix = Self;
90
91    fn key(&self) -> Vec<cw_storage_plus::Key> {
92        self.0.key()
93    }
94}
95
96impl Prefixer<'_> for AssetEntry {
97    fn prefix(&self) -> Vec<Key> {
98        self.0.prefix()
99    }
100}
101
102impl KeyDeserialize for AssetEntry {
103    type Output = AssetEntry;
104    const KEY_ELEMS: u16 = 1;
105
106    #[inline(always)]
107    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
108        Ok(AssetEntry(String::from_vec(value)?))
109    }
110}
111
112impl KeyDeserialize for &AssetEntry {
113    type Output = AssetEntry;
114    const KEY_ELEMS: u16 = AssetEntry::KEY_ELEMS;
115
116    #[inline(always)]
117    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
118        Ok(AssetEntry(String::from_vec(value)?))
119    }
120}
121
122#[cfg(test)]
123mod test {
124    #![allow(clippy::needless_borrows_for_generic_args)]
125    use rstest::rstest;
126
127    use super::*;
128
129    #[coverage_helper::test]
130    fn test_asset_entry() {
131        let mut entry = AssetEntry::new("CRAB");
132        assert_eq!(entry.as_str(), "crab");
133        entry.format();
134        assert_eq!(entry.as_str(), "crab");
135    }
136
137    #[coverage_helper::test]
138    fn test_src_chain() -> AbstractResult<()> {
139        // technically invalid, but we don't care here
140        let entry = AssetEntry::new("CRAB");
141        assert_eq!(
142            entry.src_chain(),
143            Err(AbstractError::EntryFormattingError {
144                actual: "crab".to_string(),
145                expected: "src_chain>asset_name".to_string(),
146            })
147        );
148        let entry = AssetEntry::new("osmosis>crab");
149        assert_eq!(entry.src_chain(), Ok("osmosis".to_string()));
150        let entry = AssetEntry::new("osmosis>juno>crab");
151        assert_eq!(entry.src_chain(), Ok("osmosis".to_string()));
152
153        Ok(())
154    }
155
156    #[rstest]
157    #[case("CRAB")]
158    #[case("")]
159    #[case(">")]
160    #[case("juno>")]
161    fn test_src_chain_error(#[case] input: &str) {
162        let entry = AssetEntry::new(input);
163
164        assert_eq!(
165            entry.src_chain(),
166            Err(AbstractError::EntryFormattingError {
167                actual: input.to_ascii_lowercase(),
168                expected: "src_chain>asset_name".to_string(),
169            })
170        );
171    }
172
173    #[coverage_helper::test]
174    fn test_from_string() {
175        let entry = AssetEntry::from("CRAB".to_string());
176        assert_eq!(entry.as_str(), "crab");
177    }
178
179    #[coverage_helper::test]
180    fn test_from_str() {
181        let entry = AssetEntry::from("CRAB");
182        assert_eq!(entry.as_str(), "crab");
183    }
184
185    #[coverage_helper::test]
186    fn test_from_ref_string() {
187        let entry = AssetEntry::from(&"CRAB".to_string());
188        assert_eq!(entry.as_str(), "crab");
189    }
190
191    #[coverage_helper::test]
192    fn test_to_string() {
193        let entry = AssetEntry::new("CRAB");
194        assert_eq!(entry.to_string(), "crab".to_string());
195    }
196
197    #[coverage_helper::test]
198    fn string_key_works() {
199        let k = &AssetEntry::new("CRAB");
200        let path = k.key();
201        assert_eq!(1, path.len());
202        assert_eq!(b"crab", path[0].as_ref());
203
204        let joined = k.joined_key();
205        assert_eq!(joined, b"crab")
206    }
207}