abstract_std/objects/account/
account_trace.rs1use std::fmt::Display;
2
3use super::account_id::deser::split_first_key;
4use cosmwasm_std::{ensure, Env, StdError, StdResult};
5use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
6
7use crate::{constants::CHAIN_DELIMITER, objects::TruncatedChainId, AbstractError};
8
9pub const MAX_TRACE_LENGTH: u16 = 6;
10pub(crate) const LOCAL: &str = "local";
11
12#[cosmwasm_schema::cw_serde]
21pub enum AccountTrace {
22 Local,
23 Remote(Vec<TruncatedChainId>),
25}
26
27pub const ACCOUNT_TRACE_KEY_PLACEHOLDER: &[u8] = &[];
28
29impl KeyDeserialize for &AccountTrace {
30 type Output = AccountTrace;
31 const KEY_ELEMS: u16 = MAX_TRACE_LENGTH;
32
33 #[inline(always)]
34 fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
35 let mut trace = vec![];
36 let mut value = value.as_ref();
38 for i in 0..MAX_TRACE_LENGTH - 1 {
39 let (current_chain, remainder) = split_first_key(1, value)?;
40 value = remainder;
41 if current_chain == ACCOUNT_TRACE_KEY_PLACEHOLDER {
42 continue;
43 }
44 let chain = String::from_utf8(current_chain)?;
45 if i == 0 && chain == "local" {
46 return Ok(AccountTrace::Local);
47 }
48 trace.push(TruncatedChainId::from_string(chain).unwrap())
49 }
50
51 Ok(AccountTrace::Remote(trace))
52 }
53}
54
55impl KeyDeserialize for AccountTrace {
56 type Output = AccountTrace;
57 const KEY_ELEMS: u16 = <&AccountTrace>::KEY_ELEMS;
58
59 #[inline(always)]
60 fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
61 <&AccountTrace>::from_vec(value)
62 }
63}
64
65impl PrimaryKey<'_> for AccountTrace {
66 type Prefix = ();
67 type SubPrefix = ();
68 type Suffix = Self;
69 type SuperSuffix = Self;
70
71 fn key(&self) -> Vec<cw_storage_plus::Key> {
72 let mut serialization_result = match self {
73 AccountTrace::Local => LOCAL.key(),
74 AccountTrace::Remote(chain_name) => chain_name
75 .iter()
76 .flat_map(|c| c.str_ref().key())
77 .collect::<Vec<Key>>(),
78 };
79 for _ in serialization_result.len()..(MAX_TRACE_LENGTH as usize) {
80 serialization_result.extend(ACCOUNT_TRACE_KEY_PLACEHOLDER.key());
81 }
82 serialization_result
83 }
84}
85
86impl Prefixer<'_> for AccountTrace {
87 fn prefix(&self) -> Vec<Key> {
88 self.key()
89 }
90}
91
92impl AccountTrace {
93 pub fn verify(&self) -> Result<(), AbstractError> {
95 match self {
96 AccountTrace::Local => Ok(()),
97 AccountTrace::Remote(chain_trace) => {
98 ensure!(
100 chain_trace.len() <= MAX_TRACE_LENGTH as usize,
101 AbstractError::FormattingError {
102 object: "chain-seq".into(),
103 expected: format!("between 1 and {MAX_TRACE_LENGTH}"),
104 actual: chain_trace.len().to_string(),
105 }
106 );
107 for chain in chain_trace {
108 chain.verify()?;
109 if chain.as_str().eq(LOCAL) {
110 return Err(AbstractError::FormattingError {
111 object: "chain-seq".into(),
112 expected: "not 'local'".into(),
113 actual: chain.to_string(),
114 });
115 }
116 }
117 Ok(())
118 }
119 }
120 }
121
122 pub fn verify_remote(&self) -> Result<(), AbstractError> {
124 if &Self::Local == self {
125 Err(AbstractError::Std(StdError::generic_err(
126 "expected remote account trace",
127 )))
128 } else {
129 self.verify()
130 }
131 }
132
133 pub fn verify_local(&self) -> Result<(), AbstractError> {
135 if let &Self::Remote(..) = self {
136 return Err(AbstractError::Std(StdError::generic_err(
137 "expected local account trace",
138 )));
139 }
140 Ok(())
141 }
142
143 pub fn push_local_chain(&mut self, env: &Env) {
145 match &self {
146 AccountTrace::Local => {
147 *self = AccountTrace::Remote(vec![TruncatedChainId::new(env)]);
148 }
149 AccountTrace::Remote(path) => {
150 let mut path = path.clone();
151 path.push(TruncatedChainId::new(env));
152 *self = AccountTrace::Remote(path);
153 }
154 }
155 }
156
157 pub fn push_chain(&mut self, chain_name: TruncatedChainId) {
159 match &self {
160 AccountTrace::Local => {
161 *self = AccountTrace::Remote(vec![chain_name]);
162 }
163 AccountTrace::Remote(path) => {
164 let mut path = path.clone();
165 path.push(chain_name);
166 *self = AccountTrace::Remote(path);
167 }
168 }
169 }
170
171 pub(crate) fn from_string(trace: String) -> Self {
175 account_trace_from_str(&trace)
176 }
177
178 pub(crate) fn from_str(trace: &str) -> Result<Self, AbstractError> {
179 let acc = account_trace_from_str(trace);
180 acc.verify()?;
181 Ok(acc)
182 }
183}
184
185impl TryFrom<&str> for AccountTrace {
186 type Error = AbstractError;
187
188 fn try_from(trace: &str) -> Result<Self, Self::Error> {
189 AccountTrace::from_str(trace)
190 }
191}
192
193fn account_trace_from_str(trace: &str) -> AccountTrace {
194 if trace == LOCAL {
195 AccountTrace::Local
196 } else {
197 let rev_trace: Vec<_> = trace
198 .split(CHAIN_DELIMITER.chars().next().unwrap())
200 .map(TruncatedChainId::_from_str)
201 .rev()
202 .collect();
203 AccountTrace::Remote(rev_trace)
204 }
205}
206
207impl Display for AccountTrace {
208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209 match self {
210 AccountTrace::Local => write!(f, "{}", LOCAL),
211 AccountTrace::Remote(chain_name) => write!(
212 f,
213 "{}",
214 chain_name
216 .iter()
217 .rev()
218 .map(|name| name.as_str())
219 .collect::<Vec<&str>>()
220 .join(CHAIN_DELIMITER)
221 ),
222 }
223 }
224}
225
226#[cfg(test)]
231mod test {
232 #![allow(clippy::needless_borrows_for_generic_args)]
233 use std::str::FromStr;
234
235 use cosmwasm_std::{testing::mock_dependencies, Addr, Order};
236 use cw_storage_plus::Map;
237
238 use super::*;
239
240 mod format {
241 use super::*;
242 use crate::objects::truncated_chain_id::MAX_CHAIN_NAME_LENGTH;
243
244 #[coverage_helper::test]
245 fn local_works() {
246 let trace = AccountTrace::from_str(LOCAL).unwrap();
247 assert_eq!(trace, AccountTrace::Local);
248 }
249
250 #[coverage_helper::test]
251 fn remote_works() {
252 let trace = AccountTrace::from_str("bitcoin").unwrap();
253 assert_eq!(
254 trace,
255 AccountTrace::Remote(vec![TruncatedChainId::from_str("bitcoin").unwrap()])
256 );
257 }
258
259 #[coverage_helper::test]
260 fn remote_multi_works() {
261 let trace = AccountTrace::from_str("bitcoin>ethereum").unwrap();
263 assert_eq!(
264 trace,
265 AccountTrace::Remote(vec![
267 TruncatedChainId::from_str("ethereum").unwrap(),
268 TruncatedChainId::from_str("bitcoin").unwrap(),
269 ])
270 );
271 }
272
273 #[coverage_helper::test]
274 fn remote_multi_multi_works() {
275 let trace = AccountTrace::from_str("bitcoin>ethereum>cosmos").unwrap();
277 assert_eq!(
278 trace,
279 AccountTrace::Remote(vec![
281 TruncatedChainId::from_str("cosmos").unwrap(),
282 TruncatedChainId::from_str("ethereum").unwrap(),
283 TruncatedChainId::from_str("bitcoin").unwrap(),
284 ])
285 );
286 }
287
288 #[coverage_helper::test]
290 fn local_empty_fails() {
291 AccountTrace::from_str("").unwrap_err();
292 }
293
294 #[coverage_helper::test]
295 fn local_too_short_fails() {
296 AccountTrace::from_str("a").unwrap_err();
297 }
298
299 #[coverage_helper::test]
300 fn local_too_long_fails() {
301 AccountTrace::from_str(&"a".repeat(MAX_CHAIN_NAME_LENGTH + 1)).unwrap_err();
302 }
303
304 #[coverage_helper::test]
305 fn local_uppercase_fails() {
306 AccountTrace::from_str("AAAAA").unwrap_err();
307 }
308
309 #[coverage_helper::test]
310 fn local_non_alphanumeric_fails() {
311 AccountTrace::from_str("a!aoeuoau").unwrap_err();
312 }
313 }
314
315 mod key {
316 use super::*;
317
318 fn mock_key() -> AccountTrace {
319 AccountTrace::Remote(vec![TruncatedChainId::from_str("bitcoin").unwrap()])
320 }
321
322 fn mock_local_key() -> AccountTrace {
323 AccountTrace::Local
324 }
325
326 fn mock_multi_hop_key() -> AccountTrace {
327 AccountTrace::Remote(vec![
328 TruncatedChainId::from_str("bitcoin").unwrap(),
329 TruncatedChainId::from_str("atom").unwrap(),
330 TruncatedChainId::from_str("foo").unwrap(),
331 ])
332 }
333
334 #[coverage_helper::test]
335 fn storage_key_works() {
336 let mut deps = mock_dependencies();
337 let local_key = mock_local_key();
338 let key = mock_key();
339 let multihop_key = mock_multi_hop_key();
340 let map: Map<&AccountTrace, u64> = Map::new("map");
341
342 map.save(deps.as_mut().storage, &local_key, &159784)
343 .unwrap();
344 map.save(deps.as_mut().storage, &key, &42069).unwrap();
345 map.save(deps.as_mut().storage, &multihop_key, &69420)
346 .unwrap();
347
348 assert_eq!(map.load(deps.as_ref().storage, &local_key).unwrap(), 159784);
349 assert_eq!(map.load(deps.as_ref().storage, &key).unwrap(), 42069);
350 assert_eq!(
351 map.load(deps.as_ref().storage, &multihop_key).unwrap(),
352 69420
353 );
354
355 let items = map
356 .range(deps.as_ref().storage, None, None, Order::Ascending)
357 .map(|item| item.unwrap())
358 .collect::<Vec<_>>();
359
360 assert_eq!(items.len(), 3);
361 assert_eq!(items[0], (local_key, 159784));
362 assert_eq!(items[1], (key, 42069));
363 assert_eq!(items[2], (multihop_key, 69420));
364 }
365
366 #[coverage_helper::test]
367 fn composite_key_works() {
368 let mut deps = mock_dependencies();
369 let key = mock_key();
370 let multihop_key = mock_multi_hop_key();
371 let map: Map<(&AccountTrace, Addr), u64> = Map::new("map");
372
373 map.save(
374 deps.as_mut().storage,
375 (&key, Addr::unchecked("larry")),
376 &42069,
377 )
378 .unwrap();
379 map.save(
380 deps.as_mut().storage,
381 (&multihop_key, Addr::unchecked("larry")),
382 &42069,
383 )
384 .unwrap();
385
386 map.save(
387 deps.as_mut().storage,
388 (&key, Addr::unchecked("jake")),
389 &69420,
390 )
391 .unwrap();
392
393 let items = map
394 .prefix(&key)
395 .range(deps.as_ref().storage, None, None, Order::Ascending)
396 .map(|item| item.unwrap())
397 .collect::<Vec<_>>();
398
399 assert_eq!(items.len(), 2);
400 assert_eq!(items[0], (Addr::unchecked("jake"), 69420));
401 assert_eq!(items[1], (Addr::unchecked("larry"), 42069));
402 }
403 }
404}