abstract_core/objects/entry/
contract_entry.rs

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