abstract_core/objects/account/
account_id.rs

1use std::fmt::Display;
2
3use cosmwasm_std::{StdError, StdResult};
4use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
5
6use super::{account_trace::AccountTrace, AccountSequence};
7use crate::{objects::chain_name::ChainName, AbstractError};
8
9/// Unique identifier for an account.
10/// On each chain this is unique.
11#[cosmwasm_schema::cw_serde]
12pub struct AccountId {
13    /// Sequence of the chain that triggered the IBC account creation
14    /// `AccountTrace::Local` if the account was created locally
15    /// Example: Account created on Juno which has an abstract interchain account on Osmosis,
16    /// which in turn creates an interchain account on Terra -> `AccountTrace::Remote(vec!["juno", "osmosis"])`
17    trace: AccountTrace,
18    /// Unique identifier for the accounts create on a local chain.
19    /// Is reused when creating an interchain account.
20    seq: AccountSequence,
21}
22
23impl Display for AccountId {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(f, "{}-{}", self.trace, self.seq)
26    }
27}
28
29impl AccountId {
30    pub fn new(seq: AccountSequence, trace: AccountTrace) -> Result<Self, AbstractError> {
31        trace.verify()?;
32        Ok(Self { seq, trace })
33    }
34
35    pub fn local(seq: AccountSequence) -> Self {
36        Self {
37            seq,
38            trace: AccountTrace::Local,
39        }
40    }
41
42    pub fn remote(seq: AccountSequence, trace: Vec<ChainName>) -> Result<Self, AbstractError> {
43        let trace = AccountTrace::Remote(trace);
44        trace.verify()?;
45        Ok(Self { seq, trace })
46    }
47
48    /// **Does not verify input**. Used internally for testing
49    pub const fn const_new(seq: AccountSequence, trace: AccountTrace) -> Self {
50        Self { seq, trace }
51    }
52
53    pub fn seq(&self) -> AccountSequence {
54        self.seq
55    }
56
57    pub fn trace(&self) -> &AccountTrace {
58        &self.trace
59    }
60
61    pub fn trace_mut(&mut self) -> &mut AccountTrace {
62        &mut self.trace
63    }
64
65    pub fn is_local(&self) -> bool {
66        matches!(self.trace, AccountTrace::Local)
67    }
68
69    pub fn is_remote(&self) -> bool {
70        !self.is_local()
71    }
72
73    pub fn decompose(self) -> (AccountTrace, AccountSequence) {
74        (self.trace, self.seq)
75    }
76}
77
78impl TryFrom<&str> for AccountId {
79    type Error = AbstractError;
80
81    fn try_from(value: &str) -> Result<Self, Self::Error> {
82        let (trace_str, seq_str) = value
83            .split_once('-')
84            .ok_or(AbstractError::FormattingError {
85                object: "AccountId".into(),
86                expected: "trace-999".into(),
87                actual: value.into(),
88            })?;
89        let seq: u32 = seq_str.parse().unwrap();
90        if value.starts_with(super::account_trace::LOCAL) {
91            Ok(AccountId {
92                trace: AccountTrace::Local,
93                seq,
94            })
95        } else {
96            Ok(AccountId {
97                trace: AccountTrace::from_string(trace_str.into()),
98                seq,
99            })
100        }
101    }
102}
103
104impl<'a> PrimaryKey<'a> for AccountId {
105    type Prefix = AccountTrace;
106
107    type SubPrefix = ();
108
109    type Suffix = AccountSequence;
110
111    type SuperSuffix = Self;
112
113    fn key(&self) -> Vec<cw_storage_plus::Key> {
114        let mut keys = self.trace.key();
115        keys.extend(self.seq.key());
116        keys
117    }
118}
119
120impl<'a> Prefixer<'a> for AccountId {
121    fn prefix(&self) -> Vec<Key> {
122        self.key()
123    }
124}
125
126impl KeyDeserialize for &AccountId {
127    type Output = AccountId;
128
129    #[inline(always)]
130    fn from_vec(mut value: Vec<u8>) -> StdResult<Self::Output> {
131        let mut tu = value.split_off(2);
132        let t_len = parse_length(&value)?;
133        let u = tu.split_off(t_len);
134
135        Ok(AccountId {
136            seq: AccountSequence::from_vec(u)?,
137            trace: AccountTrace::from_string(String::from_vec(tu)?),
138        })
139    }
140}
141
142impl KeyDeserialize for AccountId {
143    type Output = AccountId;
144
145    #[inline(always)]
146    fn from_vec(mut value: Vec<u8>) -> StdResult<Self::Output> {
147        let mut tu = value.split_off(2);
148        let t_len = parse_length(&value)?;
149        let u = tu.split_off(t_len);
150
151        Ok(AccountId {
152            seq: AccountSequence::from_vec(u)?,
153            trace: AccountTrace::from_string(String::from_vec(tu)?),
154        })
155    }
156}
157
158#[inline(always)]
159fn parse_length(value: &[u8]) -> StdResult<usize> {
160    Ok(u16::from_be_bytes(
161        value
162            .try_into()
163            .map_err(|_| StdError::generic_err("Could not read 2 byte length"))?,
164    )
165    .into())
166}
167
168//--------------------------------------------------------------------------------------------------
169// Tests
170//--------------------------------------------------------------------------------------------------
171
172#[cfg(test)]
173mod test {
174    use cosmwasm_std::{testing::mock_dependencies, Addr, Order};
175    use cw_storage_plus::Map;
176
177    use super::*;
178
179    mod key {
180        use std::str::FromStr;
181
182        use super::*;
183        use crate::objects::chain_name::ChainName;
184
185        fn mock_key() -> AccountId {
186            AccountId {
187                seq: 1,
188                trace: AccountTrace::Remote(vec![ChainName::from_str("bitcoin").unwrap()]),
189            }
190        }
191
192        fn mock_keys() -> (AccountId, AccountId, AccountId) {
193            (
194                AccountId {
195                    seq: 1,
196                    trace: AccountTrace::Local,
197                },
198                AccountId {
199                    seq: 1,
200                    trace: AccountTrace::Remote(vec![
201                        ChainName::from_str("ethereum").unwrap(),
202                        ChainName::from_str("bitcoin").unwrap(),
203                    ]),
204                },
205                AccountId {
206                    seq: 2,
207                    trace: AccountTrace::Remote(vec![
208                        ChainName::from_str("ethereum").unwrap(),
209                        ChainName::from_str("bitcoin").unwrap(),
210                    ]),
211                },
212            )
213        }
214
215        #[test]
216        fn storage_key_works() {
217            let mut deps = mock_dependencies();
218            let key = mock_key();
219            let map: Map<&AccountId, u64> = Map::new("map");
220
221            map.save(deps.as_mut().storage, &key, &42069).unwrap();
222
223            assert_eq!(map.load(deps.as_ref().storage, &key).unwrap(), 42069);
224
225            let items = map
226                .range(deps.as_ref().storage, None, None, Order::Ascending)
227                .map(|item| item.unwrap())
228                .collect::<Vec<_>>();
229
230            assert_eq!(items.len(), 1);
231            assert_eq!(items[0], (key, 42069));
232        }
233
234        #[test]
235        fn composite_key_works() {
236            let mut deps = mock_dependencies();
237            let key = mock_key();
238            let map: Map<(&AccountId, Addr), u64> = Map::new("map");
239
240            map.save(
241                deps.as_mut().storage,
242                (&key, Addr::unchecked("larry")),
243                &42069,
244            )
245            .unwrap();
246
247            map.save(
248                deps.as_mut().storage,
249                (&key, Addr::unchecked("jake")),
250                &69420,
251            )
252            .unwrap();
253
254            let items = map
255                .prefix(&key)
256                .range(deps.as_ref().storage, None, None, Order::Ascending)
257                .map(|item| item.unwrap())
258                .collect::<Vec<_>>();
259
260            assert_eq!(items.len(), 2);
261            assert_eq!(items[0], (Addr::unchecked("jake"), 69420));
262            assert_eq!(items[1], (Addr::unchecked("larry"), 42069));
263        }
264
265        #[test]
266        fn partial_key_works() {
267            let mut deps = mock_dependencies();
268            let (key1, key2, key3) = mock_keys();
269            let map: Map<&AccountId, u64> = Map::new("map");
270
271            map.save(deps.as_mut().storage, &key1, &42069).unwrap();
272
273            map.save(deps.as_mut().storage, &key2, &69420).unwrap();
274
275            map.save(deps.as_mut().storage, &key3, &999).unwrap();
276
277            let items = map
278                .prefix(AccountTrace::Remote(vec![
279                    ChainName::from_str("ethereum").unwrap(),
280                    ChainName::from_str("bitcoin").unwrap(),
281                ]))
282                .range(deps.as_ref().storage, None, None, Order::Ascending)
283                .map(|item| item.unwrap())
284                .collect::<Vec<_>>();
285
286            assert_eq!(items.len(), 2);
287            assert_eq!(items[0], (1, 69420));
288            assert_eq!(items[1], (2, 999));
289        }
290
291        #[test]
292        fn works_as_storage_key_with_multiple_chains_in_trace() {
293            let mut deps = mock_dependencies();
294            let key = AccountId {
295                seq: 1,
296                trace: AccountTrace::Remote(vec![
297                    ChainName::from_str("ethereum").unwrap(),
298                    ChainName::from_str("bitcoin").unwrap(),
299                ]),
300            };
301            let map: Map<&AccountId, u64> = Map::new("map");
302
303            let value = 1;
304            map.save(deps.as_mut().storage, &key, &value).unwrap();
305
306            assert_eq!(value, map.load(deps.as_ref().storage, &key).unwrap());
307        }
308    }
309
310    mod try_from {
311        // test that the try_from implementation works
312        use super::*;
313
314        #[test]
315        fn works_with_local() {
316            let account_id = AccountId::try_from("local-1").unwrap();
317            assert_eq!(account_id.seq, 1);
318            assert_eq!(account_id.trace, AccountTrace::Local);
319        }
320
321        #[test]
322        fn works_with_remote() {
323            let account_id = AccountId::try_from("ethereum>bitcoin-1").unwrap();
324            assert_eq!(account_id.seq, 1);
325            assert_eq!(
326                account_id.trace,
327                AccountTrace::Remote(vec![
328                    ChainName::_from_str("ethereum"),
329                    ChainName::_from_str("bitcoin"),
330                ])
331            );
332        }
333
334        #[test]
335        fn works_with_remote_with_multiple_chains() {
336            let account_id = AccountId::try_from("ethereum>bitcoin>cosmos-1").unwrap();
337            assert_eq!(account_id.seq, 1);
338            assert_eq!(
339                account_id.trace,
340                AccountTrace::Remote(vec![
341                    ChainName::_from_str("ethereum"),
342                    ChainName::_from_str("bitcoin"),
343                    ChainName::_from_str("cosmos"),
344                ])
345            );
346        }
347    }
348}