abstract_std/objects/entry/
contract_entry.rs

1use std::{fmt::Display, str::FromStr};
2
3use cosmwasm_std::{StdError, StdResult};
4use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
5use schemars::JsonSchema;
6use serde::{Deserialize, Serialize};
7
8use crate::constants::ATTRIBUTE_DELIMITER;
9
10/// Key to get the Address of a contract
11#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, JsonSchema, PartialOrd, Ord)]
12// Need hash for ans scraper
13#[cfg_attr(not(target_arch = "wasm32"), derive(Hash))]
14pub struct UncheckedContractEntry {
15    pub protocol: String,
16    pub contract: String,
17}
18
19impl UncheckedContractEntry {
20    pub fn new<T: ToString, R: ToString>(protocol: T, contract: R) -> Self {
21        Self {
22            protocol: protocol.to_string(),
23            contract: contract.to_string(),
24        }
25    }
26    pub fn check(self) -> ContractEntry {
27        ContractEntry {
28            contract: self.contract.to_ascii_lowercase(),
29            protocol: self.protocol.to_ascii_lowercase(),
30        }
31    }
32}
33
34impl From<ContractEntry> for UncheckedContractEntry {
35    fn from(contract_entry: ContractEntry) -> Self {
36        Self {
37            protocol: contract_entry.protocol,
38            contract: contract_entry.contract,
39        }
40    }
41}
42
43impl TryFrom<&str> for UncheckedContractEntry {
44    type Error = StdError;
45    /// Try from a string slice like "protocol:contract_name"
46    fn try_from(entry: &str) -> Result<Self, Self::Error> {
47        let Some((protocol, contract_name)) = entry.split_once(ATTRIBUTE_DELIMITER) else {
48            return Err(StdError::generic_err(
49                "contract entry should be formatted as \"protocol:contract_name\".",
50            ));
51        };
52        Ok(Self::new(protocol, contract_name))
53    }
54}
55
56/// Key to get the Address of a contract
57/// Use [`UncheckedContractEntry`] to construct this type.  
58#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, JsonSchema, Eq, PartialOrd, Ord)]
59pub struct ContractEntry {
60    pub protocol: String,
61    pub contract: String,
62}
63
64impl FromStr for ContractEntry {
65    type Err = StdError;
66
67    fn from_str(s: &str) -> Result<Self, Self::Err> {
68        UncheckedContractEntry::try_from(s).map(Into::into)
69    }
70}
71
72impl From<UncheckedContractEntry> for ContractEntry {
73    fn from(entry: UncheckedContractEntry) -> Self {
74        entry.check()
75    }
76}
77
78impl Display for ContractEntry {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(f, "{}{ATTRIBUTE_DELIMITER}{}", self.protocol, self.contract)
81    }
82}
83
84impl PrimaryKey<'_> for &ContractEntry {
85    type Prefix = String;
86
87    type SubPrefix = ();
88
89    type Suffix = String;
90
91    type SuperSuffix = Self;
92
93    fn key(&self) -> Vec<cw_storage_plus::Key> {
94        let mut keys = self.protocol.key();
95        keys.extend(self.contract.key());
96        keys
97    }
98}
99
100impl Prefixer<'_> for &ContractEntry {
101    fn prefix(&self) -> Vec<Key> {
102        let mut res = self.protocol.prefix();
103        res.extend(self.contract.prefix());
104        res
105    }
106}
107
108impl KeyDeserialize for &ContractEntry {
109    type Output = ContractEntry;
110    const KEY_ELEMS: u16 = 2;
111
112    #[inline(always)]
113    fn from_vec(mut value: Vec<u8>) -> StdResult<Self::Output> {
114        let mut tu = value.split_off(2);
115        let t_len = parse_length(&value)?;
116        let u = tu.split_off(t_len);
117
118        Ok(ContractEntry {
119            protocol: String::from_vec(tu)?,
120            contract: String::from_vec(u)?,
121        })
122    }
123}
124
125#[inline(always)]
126fn parse_length(value: &[u8]) -> StdResult<usize> {
127    Ok(u16::from_be_bytes(
128        value
129            .try_into()
130            .map_err(|_| StdError::generic_err("Could not read 2 byte length"))?,
131    )
132    .into())
133}
134
135//--------------------------------------------------------------------------------------------------
136// Tests
137//--------------------------------------------------------------------------------------------------
138
139#[cfg(test)]
140mod test {
141    #![allow(clippy::needless_borrows_for_generic_args)]
142    use cosmwasm_std::{testing::mock_dependencies, Addr, Order};
143    use cw_storage_plus::Map;
144
145    use super::*;
146
147    mod key {
148        use super::*;
149
150        fn mock_key() -> ContractEntry {
151            ContractEntry {
152                protocol: "abstract".to_string(),
153                contract: "rocket-ship".to_string(),
154            }
155        }
156
157        fn mock_keys() -> (ContractEntry, ContractEntry, ContractEntry) {
158            (
159                ContractEntry {
160                    protocol: "abstract".to_string(),
161                    contract: "sailing-ship".to_string(),
162                },
163                ContractEntry {
164                    protocol: "abstract".to_string(),
165                    contract: "rocket-ship".to_string(),
166                },
167                ContractEntry {
168                    protocol: "shitcoin".to_string(),
169                    contract: "pump'n dump".to_string(),
170                },
171            )
172        }
173
174        #[coverage_helper::test]
175        fn storage_key_works() {
176            let mut deps = mock_dependencies();
177            let key = mock_key();
178            let map: Map<&ContractEntry, u64> = Map::new("map");
179
180            map.save(deps.as_mut().storage, &key, &42069).unwrap();
181
182            assert_eq!(map.load(deps.as_ref().storage, &key).unwrap(), 42069);
183
184            let items = map
185                .range(deps.as_ref().storage, None, None, Order::Ascending)
186                .map(|item| item.unwrap())
187                .collect::<Vec<_>>();
188
189            assert_eq!(items.len(), 1);
190            assert_eq!(items[0], (key, 42069));
191        }
192
193        #[coverage_helper::test]
194        fn composite_key_works() {
195            let mut deps = mock_dependencies();
196            let key = mock_key();
197            let map: Map<(&ContractEntry, Addr), u64> = Map::new("map");
198
199            map.save(
200                deps.as_mut().storage,
201                (&key, Addr::unchecked("larry")),
202                &42069,
203            )
204            .unwrap();
205
206            map.save(
207                deps.as_mut().storage,
208                (&key, Addr::unchecked("jake")),
209                &69420,
210            )
211            .unwrap();
212
213            let items = map
214                .prefix(&key)
215                .range(deps.as_ref().storage, None, None, Order::Ascending)
216                .map(|item| item.unwrap())
217                .collect::<Vec<_>>();
218
219            assert_eq!(items.len(), 2);
220            assert_eq!(items[0], (Addr::unchecked("jake"), 69420));
221            assert_eq!(items[1], (Addr::unchecked("larry"), 42069));
222        }
223
224        #[coverage_helper::test]
225        fn partial_key_works() {
226            let mut deps = mock_dependencies();
227            let (key1, key2, key3) = mock_keys();
228            let map: Map<&ContractEntry, u64> = Map::new("map");
229
230            map.save(deps.as_mut().storage, &key1, &42069).unwrap();
231
232            map.save(deps.as_mut().storage, &key2, &69420).unwrap();
233
234            map.save(deps.as_mut().storage, &key3, &999).unwrap();
235
236            let items = map
237                .prefix("abstract".to_string())
238                .range(deps.as_ref().storage, None, None, Order::Ascending)
239                .map(|item| item.unwrap())
240                .collect::<Vec<_>>();
241
242            assert_eq!(items.len(), 2);
243            assert_eq!(items[0], ("rocket-ship".to_string(), 69420));
244            assert_eq!(items[1], ("sailing-ship".to_string(), 42069));
245        }
246
247        #[coverage_helper::test]
248        fn test_contract_entry_from_str() {
249            let contract_entry_str = "abstract:rocket-ship";
250            let contract_entry = ContractEntry::from_str(contract_entry_str).unwrap();
251
252            assert_eq!(contract_entry.protocol, "abstract");
253            assert_eq!(contract_entry.contract, "rocket-ship");
254
255            let contract_entry_str = "foo:>420/,:z/69";
256            let contract_entry = ContractEntry::from_str(contract_entry_str).unwrap();
257
258            assert_eq!(contract_entry.protocol, "foo");
259            assert_eq!(contract_entry.contract, ">420/,:z/69");
260
261            // Wrong formatting
262            let contract_entry_str = "shitcoin/,>rocket-ship";
263            let err = ContractEntry::from_str(contract_entry_str).unwrap_err();
264
265            assert_eq!(
266                err,
267                StdError::generic_err(
268                    "contract entry should be formatted as \"protocol:contract_name\".",
269                )
270            );
271        }
272
273        #[coverage_helper::test]
274        fn test_contract_entry_to_string() {
275            let contract_entry_str = "abstract:app";
276            let contract_entry = ContractEntry::from_str(contract_entry_str).unwrap();
277
278            assert_eq!(contract_entry.to_string(), contract_entry_str);
279        }
280    }
281}