abstract_std/objects/account/
account_trace.rs

1use 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/// The identifier of chain that triggered the account creation
13///
14/// Note that the serialization to string and to Cw-storage-plus keys is different
15///
16/// For String, `AccountTrace::Remote(vec!["neutron", "osmosis"])` will be serialized as `osmosis>neutron`
17///
18/// For cw-storage-plus-key, `AccountTrace::Remote(vec!["neutron", "osmosis"])` will be serialized as `remote:["neutron", "osmosis", "", "", "", ""]`
19
20#[cosmwasm_schema::cw_serde]
21pub enum AccountTrace {
22    Local,
23    // path of the chains that triggered the account creation
24    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        // We parse the whole data for the MAX_TRACE_LENGTH keys
37        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    /// verify the formatting of the Account trace chain
94    pub fn verify(&self) -> Result<(), AbstractError> {
95        match self {
96            AccountTrace::Local => Ok(()),
97            AccountTrace::Remote(chain_trace) => {
98                // Ensure the trace length is limited
99                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    /// assert that the account trace is a remote account and verify the formatting
123    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    /// assert that the trace is local
134    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    /// push the `env.block.chain_name` to the chain trace
144    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    /// push a chain name to the account's path
158    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    /// **No verification is done here**
172    ///
173    /// **only use this for deserialization**
174    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            // DoubleEndedSearcher implemented for char, but not for "str"
199            .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                // "juno>terra>osmosis"
215                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//--------------------------------------------------------------------------------------------------
227// Tests
228//--------------------------------------------------------------------------------------------------
229
230#[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            // Here the account originates from ethereum and was then bridged to bitcoin
262            let trace = AccountTrace::from_str("bitcoin>ethereum").unwrap();
263            assert_eq!(
264                trace,
265                // The trace vector pushes the last chains last
266                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            // Here the account originates from cosmos, and was then bridged to ethereum and was then bridged to bitcoin
276            let trace = AccountTrace::from_str("bitcoin>ethereum>cosmos").unwrap();
277            assert_eq!(
278                trace,
279                // The trace vector pushes the last chains last
280                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        // now test failures
289        #[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}