abstract_core/objects/account/
account_trace.rs

1use std::fmt::Display;
2
3use cosmwasm_std::{ensure, Env, StdError, StdResult};
4use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
5
6use crate::{constants::CHAIN_DELIMITER, objects::chain_name::ChainName, AbstractError};
7
8pub const MAX_TRACE_LENGTH: usize = 6;
9pub(crate) const LOCAL: &str = "local";
10
11/// The identifier of chain that triggered the account creation
12#[cosmwasm_schema::cw_serde]
13pub enum AccountTrace {
14    Local,
15    // path of the chains that triggered the account creation
16    Remote(Vec<ChainName>),
17}
18
19impl KeyDeserialize for &AccountTrace {
20    type Output = AccountTrace;
21    #[inline(always)]
22    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
23        Ok(AccountTrace::from_string(String::from_vec(value)?))
24    }
25}
26
27impl<'a> PrimaryKey<'a> for AccountTrace {
28    type Prefix = ();
29    type SubPrefix = ();
30    type Suffix = Self;
31    type SuperSuffix = Self;
32
33    fn key(&self) -> Vec<cw_storage_plus::Key> {
34        match self {
35            AccountTrace::Local => LOCAL.key(),
36            AccountTrace::Remote(chain_name) => {
37                let len = chain_name.len();
38                chain_name
39                    .iter()
40                    .enumerate()
41                    .flat_map(|(s, c)| {
42                        if s == len - 1 {
43                            vec![c.str_ref().key()]
44                        } else {
45                            vec![c.str_ref().key(), CHAIN_DELIMITER.key()]
46                        }
47                    })
48                    .flatten()
49                    .collect::<Vec<Key>>()
50            }
51        }
52    }
53}
54
55impl KeyDeserialize for AccountTrace {
56    type Output = AccountTrace;
57    #[inline(always)]
58    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
59        Ok(AccountTrace::from_string(String::from_vec(value)?))
60    }
61}
62
63impl<'a> Prefixer<'a> for AccountTrace {
64    fn prefix(&self) -> Vec<Key> {
65        self.key()
66    }
67}
68
69impl AccountTrace {
70    /// verify the formatting of the Account trace chain
71    pub fn verify(&self) -> Result<(), AbstractError> {
72        match self {
73            AccountTrace::Local => Ok(()),
74            AccountTrace::Remote(chain_trace) => {
75                // Ensure the trace length is limited
76                ensure!(
77                    chain_trace.len() <= MAX_TRACE_LENGTH,
78                    AbstractError::FormattingError {
79                        object: "chain-seq".into(),
80                        expected: format!("between 1 and {MAX_TRACE_LENGTH}"),
81                        actual: chain_trace.len().to_string(),
82                    }
83                );
84                for chain in chain_trace {
85                    chain.verify()?;
86                    if chain.as_str().eq(LOCAL) {
87                        return Err(AbstractError::FormattingError {
88                            object: "chain-seq".into(),
89                            expected: "not 'local'".into(),
90                            actual: chain.to_string(),
91                        });
92                    }
93                }
94                Ok(())
95            }
96        }
97    }
98
99    /// assert that the account trace is a remote account and verify the formatting
100    pub fn verify_remote(&self) -> Result<(), AbstractError> {
101        if &Self::Local == self {
102            Err(AbstractError::Std(StdError::generic_err(
103                "expected remote account trace",
104            )))
105        } else {
106            self.verify()
107        }
108    }
109
110    /// assert that the trace is local
111    pub fn verify_local(&self) -> Result<(), AbstractError> {
112        if let &Self::Remote(..) = self {
113            return Err(AbstractError::Std(StdError::generic_err(
114                "expected local account trace",
115            )));
116        }
117        Ok(())
118    }
119
120    /// push the `env.block.chain_name` to the chain trace
121    pub fn push_local_chain(&mut self, env: &Env) {
122        match &self {
123            AccountTrace::Local => {
124                *self = AccountTrace::Remote(vec![ChainName::new(env)]);
125            }
126            AccountTrace::Remote(path) => {
127                let mut path = path.clone();
128                path.push(ChainName::new(env));
129                *self = AccountTrace::Remote(path);
130            }
131        }
132    }
133    /// push a chain name to the account's path
134    pub fn push_chain(&mut self, chain_name: ChainName) {
135        match &self {
136            AccountTrace::Local => {
137                *self = AccountTrace::Remote(vec![chain_name]);
138            }
139            AccountTrace::Remote(path) => {
140                let mut path = path.clone();
141                path.push(chain_name);
142                *self = AccountTrace::Remote(path);
143            }
144        }
145    }
146
147    /// **No verification is done here**
148    ///
149    /// **only use this for deserialization**
150    pub(crate) fn from_string(trace: String) -> Self {
151        let acc = if trace == LOCAL {
152            Self::Local
153        } else {
154            Self::Remote(
155                trace
156                    .split(CHAIN_DELIMITER)
157                    .map(ChainName::_from_str)
158                    .collect(),
159            )
160        };
161        acc
162    }
163
164    /// **No verification is done here**
165    ///
166    /// **only use this for deserialization**
167    #[allow(unused)]
168    pub(crate) fn from_str(trace: &str) -> Result<Self, AbstractError> {
169        let acc = if trace == LOCAL {
170            Self::Local
171        } else {
172            Self::Remote(
173                trace
174                    .split(CHAIN_DELIMITER)
175                    .map(ChainName::_from_str)
176                    .collect(),
177            )
178        };
179        acc.verify()?;
180        Ok(acc)
181    }
182}
183
184impl TryFrom<&str> for AccountTrace {
185    type Error = AbstractError;
186
187    fn try_from(trace: &str) -> Result<Self, Self::Error> {
188        if trace == LOCAL {
189            Ok(Self::Local)
190        } else {
191            let chain_trace: Vec<ChainName> = trace
192                .split(CHAIN_DELIMITER)
193                .map(|t| ChainName::from_string(t.to_string()))
194                .collect::<Result<Vec<_>, _>>()?;
195            Ok(Self::Remote(chain_trace))
196        }
197    }
198}
199
200impl Display for AccountTrace {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        match self {
203            AccountTrace::Local => write!(f, "{}", LOCAL),
204            AccountTrace::Remote(chain_name) => write!(
205                f,
206                "{}",
207                // "juno>terra>osmosis"
208                chain_name
209                    .iter()
210                    .map(|name| name.as_str())
211                    .collect::<Vec<&str>>()
212                    .join(CHAIN_DELIMITER)
213            ),
214        }
215    }
216}
217
218//--------------------------------------------------------------------------------------------------
219// Tests
220//--------------------------------------------------------------------------------------------------
221
222#[cfg(test)]
223mod test {
224    use std::str::FromStr;
225
226    use cosmwasm_std::{testing::mock_dependencies, Addr, Order};
227    use cw_storage_plus::Map;
228
229    use super::*;
230
231    mod format {
232        use super::*;
233        use crate::objects::chain_name::MAX_CHAIN_NAME_LENGTH;
234
235        #[test]
236        fn local_works() {
237            let trace = AccountTrace::from_str(LOCAL).unwrap();
238            assert_eq!(trace, AccountTrace::Local);
239        }
240
241        #[test]
242        fn remote_works() {
243            let trace = AccountTrace::from_str("bitcoin").unwrap();
244            assert_eq!(
245                trace,
246                AccountTrace::Remote(vec![ChainName::from_str("bitcoin").unwrap()])
247            );
248        }
249
250        #[test]
251        fn remote_multi_works() {
252            let trace = AccountTrace::from_str("bitcoin>ethereum").unwrap();
253            assert_eq!(
254                trace,
255                AccountTrace::Remote(vec![
256                    ChainName::from_str("bitcoin").unwrap(),
257                    ChainName::from_str("ethereum").unwrap()
258                ])
259            );
260        }
261
262        #[test]
263        fn remote_multi_multi_works() {
264            let trace = AccountTrace::from_str("bitcoin>ethereum>cosmos").unwrap();
265            assert_eq!(
266                trace,
267                AccountTrace::Remote(vec![
268                    ChainName::from_str("bitcoin").unwrap(),
269                    ChainName::from_str("ethereum").unwrap(),
270                    ChainName::from_str("cosmos").unwrap(),
271                ])
272            );
273        }
274
275        // now test failures
276        #[test]
277        fn local_empty_fails() {
278            AccountTrace::from_str("").unwrap_err();
279        }
280
281        #[test]
282        fn local_too_short_fails() {
283            AccountTrace::from_str("a").unwrap_err();
284        }
285
286        #[test]
287        fn local_too_long_fails() {
288            AccountTrace::from_str(&"a".repeat(MAX_CHAIN_NAME_LENGTH + 1)).unwrap_err();
289        }
290
291        #[test]
292        fn local_uppercase_fails() {
293            AccountTrace::from_str("AAAAA").unwrap_err();
294        }
295
296        #[test]
297        fn local_non_alphanumeric_fails() {
298            AccountTrace::from_str("a!aoeuoau").unwrap_err();
299        }
300    }
301
302    mod key {
303        use super::*;
304
305        fn mock_key() -> AccountTrace {
306            AccountTrace::Remote(vec![ChainName::from_str("bitcoin").unwrap()])
307        }
308
309        #[test]
310        fn storage_key_works() {
311            let mut deps = mock_dependencies();
312            let key = mock_key();
313            let map: Map<&AccountTrace, u64> = Map::new("map");
314
315            map.save(deps.as_mut().storage, &key, &42069).unwrap();
316
317            assert_eq!(map.load(deps.as_ref().storage, &key).unwrap(), 42069);
318
319            let items = map
320                .range(deps.as_ref().storage, None, None, Order::Ascending)
321                .map(|item| item.unwrap())
322                .collect::<Vec<_>>();
323
324            assert_eq!(items.len(), 1);
325            assert_eq!(items[0], (key, 42069));
326        }
327
328        #[test]
329        fn composite_key_works() {
330            let mut deps = mock_dependencies();
331            let key = mock_key();
332            let map: Map<(&AccountTrace, Addr), u64> = Map::new("map");
333
334            map.save(
335                deps.as_mut().storage,
336                (&key, Addr::unchecked("larry")),
337                &42069,
338            )
339            .unwrap();
340
341            map.save(
342                deps.as_mut().storage,
343                (&key, Addr::unchecked("jake")),
344                &69420,
345            )
346            .unwrap();
347
348            let items = map
349                .prefix(&key)
350                .range(deps.as_ref().storage, None, None, Order::Ascending)
351                .map(|item| item.unwrap())
352                .collect::<Vec<_>>();
353
354            assert_eq!(items.len(), 2);
355            assert_eq!(items[0], (Addr::unchecked("jake"), 69420));
356            assert_eq!(items[1], (Addr::unchecked("larry"), 42069));
357        }
358    }
359}