abstract_core/objects/entry/
contract_entry.rs1use 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#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, JsonSchema, PartialOrd, Ord)]
16#[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 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#[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#[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 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}