abstract_core/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<'a> PrimaryKey<'a> for &AssetEntry {
83    type Prefix = ();
84
85    type SubPrefix = ();
86
87    type Suffix = Self;
88
89    type SuperSuffix = Self;
90
91    // TODO: make this key implementation use src_chain as prefix
92    fn key(&self) -> Vec<cw_storage_plus::Key> {
93        self.0.key()
94    }
95}
96
97impl<'a> Prefixer<'a> for &AssetEntry {
98    fn prefix(&self) -> Vec<Key> {
99        self.0.prefix()
100    }
101}
102
103impl KeyDeserialize for &AssetEntry {
104    type Output = AssetEntry;
105
106    #[inline(always)]
107    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
108        Ok(AssetEntry(String::from_vec(value)?))
109    }
110}
111
112#[cfg(test)]
113mod test {
114    use rstest::rstest;
115    use speculoos::prelude::*;
116
117    use super::*;
118
119    #[test]
120    fn test_asset_entry() {
121        let mut entry = AssetEntry::new("CRAB");
122        assert_that!(entry.as_str()).is_equal_to("crab");
123        entry.format();
124        assert_that!(entry.as_str()).is_equal_to("crab");
125    }
126
127    #[test]
128    fn test_src_chain() -> AbstractResult<()> {
129        // technically invalid, but we don't care here
130        let entry = AssetEntry::new("CRAB");
131        assert_that!(entry.src_chain())
132            .is_err()
133            .is_equal_to(AbstractError::EntryFormattingError {
134                actual: "crab".to_string(),
135                expected: "src_chain>asset_name".to_string(),
136            });
137        let entry = AssetEntry::new("osmosis>crab");
138        assert_that!(entry.src_chain())
139            .is_ok()
140            .is_equal_to("osmosis".to_string());
141        let entry = AssetEntry::new("osmosis>juno>crab");
142        assert_that!(entry.src_chain())
143            .is_ok()
144            .is_equal_to("osmosis".to_string());
145
146        Ok(())
147    }
148
149    #[rstest]
150    #[case("CRAB")]
151    #[case("")]
152    #[case(">")]
153    #[case("juno>")]
154    fn test_src_chain_error(#[case] input: &str) {
155        let entry = AssetEntry::new(input);
156
157        assert_that!(entry.src_chain())
158            .is_err()
159            .is_equal_to(AbstractError::EntryFormattingError {
160                actual: input.to_ascii_lowercase(),
161                expected: "src_chain>asset_name".to_string(),
162            });
163    }
164
165    #[test]
166    fn test_from_string() {
167        let entry = AssetEntry::from("CRAB".to_string());
168        assert_that!(entry.as_str()).is_equal_to("crab");
169    }
170
171    #[test]
172    fn test_from_str() {
173        let entry = AssetEntry::from("CRAB");
174        assert_that!(entry.as_str()).is_equal_to("crab");
175    }
176
177    #[test]
178    fn test_from_ref_string() {
179        let entry = AssetEntry::from(&"CRAB".to_string());
180        assert_that!(entry.as_str()).is_equal_to("crab");
181    }
182
183    #[test]
184    fn test_to_string() {
185        let entry = AssetEntry::new("CRAB");
186        assert_that!(entry.to_string()).is_equal_to("crab".to_string());
187    }
188
189    #[test]
190    fn string_key_works() {
191        let k = &AssetEntry::new("CRAB");
192        let path = k.key();
193        assert_eq!(1, path.len());
194        assert_eq!(b"crab", path[0].as_ref());
195
196        let joined = k.joined_key();
197        assert_eq!(joined, b"crab")
198    }
199}