abstract_std/objects/account/
account_id.rs

1use std::{fmt::Display, str::FromStr};
2
3use cosmwasm_std::StdResult;
4use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
5use deser::split_first_key;
6
7use super::{account_trace::AccountTrace, AccountSequence};
8use crate::{objects::TruncatedChainId, AbstractError};
9
10/// Unique identifier for an account.
11/// On each chain this is unique.
12#[cosmwasm_schema::cw_serde]
13pub struct AccountId {
14    /// Sequence of the chain that triggered the IBC account creation
15    /// `AccountTrace::Local` if the account was created locally
16    /// Example: Account created on Juno which has an abstract interchain account on Osmosis,
17    /// which in turn creates an interchain account on Terra -> `AccountTrace::Remote(vec!["juno", "osmosis"])`
18    trace: AccountTrace,
19    /// Unique identifier for the accounts create on a local chain.
20    /// Is reused when creating an interchain account.
21    seq: AccountSequence,
22}
23
24impl Display for AccountId {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(f, "{}-{}", self.trace, self.seq)
27    }
28}
29
30impl AccountId {
31    pub fn new(seq: AccountSequence, trace: AccountTrace) -> Result<Self, AbstractError> {
32        trace.verify()?;
33        Ok(Self { seq, trace })
34    }
35
36    pub fn local(seq: AccountSequence) -> Self {
37        Self {
38            seq,
39            trace: AccountTrace::Local,
40        }
41    }
42
43    pub fn remote(
44        seq: AccountSequence,
45        trace: Vec<TruncatedChainId>,
46    ) -> Result<Self, AbstractError> {
47        let trace = AccountTrace::Remote(trace);
48        trace.verify()?;
49        Ok(Self { seq, trace })
50    }
51
52    /// Construct the `AccountId` for an account on a host chain based on the current Account.
53    /// Will pop the trace if the host chain is the last chain in the trace.
54    pub fn into_remote_account_id(
55        mut self,
56        client_chain: TruncatedChainId,
57        host_chain: TruncatedChainId,
58    ) -> Self {
59        match &mut self.trace {
60            AccountTrace::Remote(ref mut chains) => {
61                // if last account chain is the host chain, pop
62                if chains.last() != Some(&host_chain) {
63                    chains.push(client_chain);
64                } else {
65                    chains.pop();
66                    // if the pop made the AccountId empty then we're targeting a local account.
67                    if chains.is_empty() {
68                        self.trace = AccountTrace::Local;
69                    }
70                }
71            }
72            AccountTrace::Local => {
73                self.trace = AccountTrace::Remote(vec![client_chain]);
74            }
75        }
76        self
77    }
78
79    /// **Does not verify input**. Used internally for testing
80    pub const fn const_new(seq: AccountSequence, trace: AccountTrace) -> Self {
81        Self { seq, trace }
82    }
83
84    pub fn seq(&self) -> AccountSequence {
85        self.seq
86    }
87
88    pub fn trace(&self) -> &AccountTrace {
89        &self.trace
90    }
91
92    pub fn trace_mut(&mut self) -> &mut AccountTrace {
93        &mut self.trace
94    }
95
96    pub fn is_local(&self) -> bool {
97        matches!(self.trace, AccountTrace::Local)
98    }
99
100    pub fn is_remote(&self) -> bool {
101        !self.is_local()
102    }
103
104    /// Push the chain to the account trace
105    pub fn push_chain(&mut self, chain: TruncatedChainId) {
106        self.trace_mut().push_chain(chain)
107    }
108
109    pub fn decompose(self) -> (AccountTrace, AccountSequence) {
110        (self.trace, self.seq)
111    }
112}
113
114impl FromStr for AccountId {
115    type Err = AbstractError;
116
117    fn from_str(value: &str) -> Result<Self, Self::Err> {
118        let (trace_str, seq_str) = value
119            .split_once('-')
120            .ok_or(AbstractError::FormattingError {
121                object: "AccountId".into(),
122                expected: "trace-999".into(),
123                actual: value.into(),
124            })?;
125        let seq: u32 = seq_str.parse().unwrap();
126        if value.starts_with(super::account_trace::LOCAL) {
127            Ok(AccountId {
128                trace: AccountTrace::Local,
129                seq,
130            })
131        } else {
132            Ok(AccountId {
133                trace: AccountTrace::from_string(trace_str.into()),
134                seq,
135            })
136        }
137    }
138}
139
140impl PrimaryKey<'_> for AccountId {
141    type Prefix = AccountTrace;
142
143    type SubPrefix = ();
144
145    type Suffix = AccountSequence;
146
147    type SuperSuffix = Self;
148
149    fn key(&self) -> Vec<cw_storage_plus::Key> {
150        let mut keys = self.trace.key();
151        keys.extend(self.seq.key());
152        keys
153    }
154}
155
156impl Prefixer<'_> for AccountId {
157    fn prefix(&self) -> Vec<Key> {
158        self.key()
159    }
160}
161
162impl KeyDeserialize for &AccountId {
163    type Output = AccountId;
164    const KEY_ELEMS: u16 = AccountTrace::KEY_ELEMS + u32::KEY_ELEMS;
165
166    #[inline(always)]
167    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
168        let (trace, seq) = split_first_key(AccountTrace::KEY_ELEMS, value.as_ref())?;
169
170        Ok(AccountId {
171            seq: AccountSequence::from_vec(seq.to_vec())?,
172            trace: AccountTrace::from_vec(trace)?,
173        })
174    }
175}
176
177impl KeyDeserialize for AccountId {
178    type Output = AccountId;
179    const KEY_ELEMS: u16 = <&AccountId>::KEY_ELEMS;
180
181    #[inline(always)]
182    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
183        <&AccountId>::from_vec(value)
184    }
185}
186
187/// This was copied from cosmwasm-std
188///
189/// https://github.com/CosmWasm/cw-storage-plus/blob/f65cd4000a0dc1c009f3f99e23f9e10a1c256a68/src/de.rs#L173
190pub(crate) mod deser {
191    use cosmwasm_std::{StdError, StdResult};
192
193    /// Splits the first key from the value based on the provided number of key elements.
194    /// The return value is ordered as (first_key, remainder).
195    ///
196    pub fn split_first_key(key_elems: u16, value: &[u8]) -> StdResult<(Vec<u8>, &[u8])> {
197        let mut index = 0;
198        let mut first_key = Vec::new();
199
200        // Iterate over the sub keys
201        for i in 0..key_elems {
202            let len_slice = &value[index..index + 2];
203            index += 2;
204            let is_last_key = i == key_elems - 1;
205
206            if !is_last_key {
207                first_key.extend_from_slice(len_slice);
208            }
209
210            let subkey_len = parse_length(len_slice)?;
211            first_key.extend_from_slice(&value[index..index + subkey_len]);
212            index += subkey_len;
213        }
214
215        let remainder = &value[index..];
216        Ok((first_key, remainder))
217    }
218
219    fn parse_length(value: &[u8]) -> StdResult<usize> {
220        Ok(u16::from_be_bytes(
221            value
222                .try_into()
223                .map_err(|_| StdError::generic_err("Could not read 2 byte length"))?,
224        )
225        .into())
226    }
227}
228//--------------------------------------------------------------------------------------------------
229// Tests
230//--------------------------------------------------------------------------------------------------
231
232#[cfg(test)]
233mod test {
234    #![allow(clippy::needless_borrows_for_generic_args)]
235    use cosmwasm_std::{testing::mock_dependencies, Addr, Order};
236    use cw_storage_plus::Map;
237
238    use super::*;
239
240    mod key {
241        use super::*;
242
243        use std::str::FromStr;
244
245        fn mock_key() -> AccountId {
246            AccountId {
247                seq: 1,
248                trace: AccountTrace::Remote(vec![TruncatedChainId::from_str("bitcoin").unwrap()]),
249            }
250        }
251
252        fn mock_local_key() -> AccountId {
253            AccountId {
254                seq: 54,
255                trace: AccountTrace::Local,
256            }
257        }
258
259        fn mock_keys() -> (AccountId, AccountId, AccountId) {
260            (
261                AccountId {
262                    seq: 1,
263                    trace: AccountTrace::Local,
264                },
265                AccountId {
266                    seq: 1,
267                    trace: AccountTrace::Remote(vec![
268                        TruncatedChainId::from_str("ethereum").unwrap(),
269                        TruncatedChainId::from_str("bitcoin").unwrap(),
270                    ]),
271                },
272                AccountId {
273                    seq: 2,
274                    trace: AccountTrace::Remote(vec![
275                        TruncatedChainId::from_str("ethereum").unwrap(),
276                        TruncatedChainId::from_str("bitcoin").unwrap(),
277                    ]),
278                },
279            )
280        }
281
282        #[coverage_helper::test]
283        fn storage_key_works() {
284            let mut deps = mock_dependencies();
285            let key = mock_key();
286            let map: Map<&AccountId, u64> = Map::new("map");
287
288            map.save(deps.as_mut().storage, &key, &42069).unwrap();
289
290            assert_eq!(map.load(deps.as_ref().storage, &key).unwrap(), 42069);
291
292            let items = map
293                .range(deps.as_ref().storage, None, None, Order::Ascending)
294                .map(|item| item.unwrap())
295                .collect::<Vec<_>>();
296
297            assert_eq!(items.len(), 1);
298            assert_eq!(items[0], (key, 42069));
299        }
300
301        #[coverage_helper::test]
302        fn storage_key_local_works() {
303            let mut deps = mock_dependencies();
304            let key = mock_local_key();
305            let map: Map<&AccountId, u64> = Map::new("map");
306
307            map.save(deps.as_mut().storage, &key, &42069).unwrap();
308
309            assert_eq!(map.load(deps.as_ref().storage, &key).unwrap(), 42069);
310
311            let items = map
312                .range(deps.as_ref().storage, None, None, Order::Ascending)
313                .map(|item| item.unwrap())
314                .collect::<Vec<_>>();
315
316            assert_eq!(items.len(), 1);
317            assert_eq!(items[0], (key, 42069));
318        }
319
320        #[coverage_helper::test]
321        fn composite_key_works() {
322            let mut deps = mock_dependencies();
323            let key = mock_key();
324            let map: Map<(&AccountId, Addr), u64> = Map::new("map");
325
326            map.save(
327                deps.as_mut().storage,
328                (&key, Addr::unchecked("larry")),
329                &42069,
330            )
331            .unwrap();
332
333            map.save(
334                deps.as_mut().storage,
335                (&key, Addr::unchecked("jake")),
336                &69420,
337            )
338            .unwrap();
339
340            let items = map
341                .prefix(&key)
342                .range(deps.as_ref().storage, None, None, Order::Ascending)
343                .map(|item| item.unwrap())
344                .collect::<Vec<_>>();
345
346            assert_eq!(items.len(), 2);
347            assert_eq!(items[0], (Addr::unchecked("jake"), 69420));
348            assert_eq!(items[1], (Addr::unchecked("larry"), 42069));
349        }
350
351        #[coverage_helper::test]
352        fn partial_key_works() {
353            let mut deps = mock_dependencies();
354            let (key1, key2, key3) = mock_keys();
355            let map: Map<&AccountId, u64> = Map::new("map");
356
357            map.save(deps.as_mut().storage, &key1, &42069).unwrap();
358
359            map.save(deps.as_mut().storage, &key2, &69420).unwrap();
360
361            map.save(deps.as_mut().storage, &key3, &999).unwrap();
362
363            let items = map
364                .prefix(AccountTrace::Remote(vec![
365                    TruncatedChainId::from_str("ethereum").unwrap(),
366                    TruncatedChainId::from_str("bitcoin").unwrap(),
367                ]))
368                .range(deps.as_ref().storage, None, None, Order::Ascending)
369                .map(|item| item.unwrap())
370                .collect::<Vec<_>>();
371
372            assert_eq!(items.len(), 2);
373            assert_eq!(items[0], (1, 69420));
374            assert_eq!(items[1], (2, 999));
375        }
376
377        #[coverage_helper::test]
378        fn works_as_storage_key_with_multiple_chains_in_trace() {
379            let mut deps = mock_dependencies();
380            let key = AccountId {
381                seq: 1,
382                trace: AccountTrace::Remote(vec![
383                    TruncatedChainId::from_str("ethereum").unwrap(),
384                    TruncatedChainId::from_str("bitcoin").unwrap(),
385                ]),
386            };
387            let map: Map<&AccountId, u64> = Map::new("map");
388
389            let value = 1;
390            map.save(deps.as_mut().storage, &key, &value).unwrap();
391
392            assert_eq!(value, map.load(deps.as_ref().storage, &key).unwrap());
393        }
394    }
395
396    mod from_str {
397        // test that the try_from implementation works
398        use super::*;
399
400        #[coverage_helper::test]
401        fn works_with_local() {
402            let account_id: AccountId = "local-1".parse().unwrap();
403            assert_eq!(account_id.seq, 1);
404            assert_eq!(account_id.trace, AccountTrace::Local);
405        }
406
407        #[coverage_helper::test]
408        fn works_with_remote() {
409            let account_id: AccountId = "ethereum>bitcoin-1".parse().unwrap();
410            assert_eq!(account_id.seq, 1);
411            assert_eq!(
412                account_id.trace,
413                AccountTrace::Remote(vec![
414                    TruncatedChainId::_from_str("bitcoin"),
415                    TruncatedChainId::_from_str("ethereum"),
416                ])
417            );
418        }
419
420        #[coverage_helper::test]
421        fn works_with_remote_with_multiple_chains() {
422            let account_id: AccountId = "ethereum>bitcoin>cosmos-1".parse().unwrap();
423            assert_eq!(account_id.seq, 1);
424            assert_eq!(
425                account_id.trace,
426                AccountTrace::Remote(vec![
427                    TruncatedChainId::_from_str("cosmos"),
428                    TruncatedChainId::_from_str("bitcoin"),
429                    TruncatedChainId::_from_str("ethereum"),
430                ])
431            );
432        }
433    }
434}