andromeda_std/amp/
addresses.rs

1use std::fmt::{Display, Formatter, Result as FMTResult};
2
3use crate::error::ContractError;
4use crate::os::vfs::{vfs_resolve_symlink, PATH_REGEX, PROTOCOL_PATH_REGEX};
5use crate::{ado_contract::ADOContract, os::vfs::vfs_resolve_path};
6use cosmwasm_schema::cw_serde;
7use cosmwasm_std::{Addr, Api, Deps, QuerierWrapper, Storage};
8use lazy_static::lazy_static;
9
10lazy_static! {
11    static ref ANDR_ADDR_REGEX: String = format!(
12        // Combine all valid regex for ANDR_ADDR schema validations
13        "({re1})|({re2})|({re3})|({re4})",
14        // Protocol regex
15        re1 = PROTOCOL_PATH_REGEX,
16        // Path regex
17        re2 = PATH_REGEX,
18        // Raw address
19        re3 = r"^[a-z0-9]{2,}$",
20        // Local path
21        re4 = r"^\.(/[A-Za-z0-9.\-_]{2,40}?)*(/)?$",
22    );
23}
24
25/// An address that can be used within the Andromeda ecosystem.
26/// Inspired by the cosmwasm-std `Addr` type. https://github.com/CosmWasm/cosmwasm/blob/2a1c698520a1aacedfe3f4803b0d7d653892217a/packages/std/src/addresses.rs#L33
27///
28/// This address can be one of two things:
29/// 1. A valid human readable address e.g. `cosmos1...`
30/// 2. A valid Andromeda VFS path e.g. `/home/user/app/component`
31///
32/// VFS paths can be local in the case of an app and can be done by referencing `./component` they can also contain protocols for cross chain communication. A VFS path is usually structured as so:
33///
34/// `<protocol>://<chain (required if ibc used)>/<path>` or `ibc://cosmoshub-4/user/app/component`
35#[cw_serde]
36pub struct AndrAddr(#[schemars(regex = "ANDR_ADDR_REGEX")] String);
37
38impl AndrAddr {
39    #[inline]
40    pub fn as_str(&self) -> &str {
41        self.0.as_str()
42    }
43
44    #[inline]
45    pub fn as_bytes(&self) -> &[u8] {
46        self.0.as_bytes()
47    }
48
49    #[inline]
50    pub fn into_string(self) -> String {
51        self.0
52    }
53
54    #[inline]
55    pub fn from_string(addr: impl Into<String>) -> AndrAddr {
56        AndrAddr(addr.into())
57    }
58
59    #[inline]
60    pub fn to_lowercase(&self) -> AndrAddr {
61        AndrAddr(self.0.to_lowercase())
62    }
63
64    /// Validates an `AndrAddr`, to be valid the given address must either be a human readable address or a valid VFS path.
65    ///
66    /// **The existence of the provided path is not validated.**
67    ///
68    /// **If you wish to validate the existence of the path you must use `get_raw_address`.**
69    pub fn validate(&self, api: &dyn Api) -> Result<(), ContractError> {
70        match self.is_vfs_path() || self.is_addr(api) {
71            true => Ok(()),
72            false => Err(ContractError::InvalidAddress {}),
73        }
74    }
75
76    /// Retrieves the raw address represented by the AndrAddr.
77    ///
78    /// If the address is a valid human readable address then that is returned, otherwise it is assumed to be a Andromeda VFS path and is resolved accordingly.
79    ///
80    /// If the address is assumed to be a VFS path and no VFS contract address is provided then an appropriate error is returned.
81    pub fn get_raw_address(&self, deps: &Deps) -> Result<Addr, ContractError> {
82        if !self.is_vfs_path() {
83            return Ok(deps.api.addr_validate(&self.0)?);
84        }
85
86        let contract = ADOContract::default();
87        let vfs_contract = contract.get_vfs_address(deps.storage, &deps.querier)?;
88        self.get_raw_address_from_vfs(deps, vfs_contract)
89    }
90
91    /// Retrieves the raw address represented by the AndrAddr from the given VFS contract.
92    ///     
93    /// If the address is a valid human readable address then that is returned, otherwise it is assumed to be a Andromeda VFS path and is resolved accordingly.
94    ///
95    /// If the address is assumed to be a VFS path and no VFS contract address is provided then an appropriate error is returned.
96    pub fn get_raw_address_from_vfs(
97        &self,
98        deps: &Deps,
99        vfs_contract: impl Into<String>,
100    ) -> Result<Addr, ContractError> {
101        match self.is_vfs_path() {
102            false => Ok(deps.api.addr_validate(&self.0)?),
103            true => {
104                let vfs_contract: String = vfs_contract.into();
105                // Convert local path to VFS path before querying
106                let valid_vfs_path =
107                    self.local_path_to_vfs_path(deps.storage, &deps.querier, vfs_contract.clone())?;
108                let vfs_addr = Addr::unchecked(vfs_contract);
109                vfs_resolve_path(valid_vfs_path.clone(), vfs_addr, &deps.querier)
110                    .ok()
111                    .ok_or(ContractError::InvalidPathname {
112                        error: Some(format!(
113                            "{:?} does not exist in the file system",
114                            valid_vfs_path.0
115                        )),
116                    })
117            }
118        }
119    }
120
121    /// Converts a local path to a valid VFS path by replacing `./` with the app contract address
122    pub fn local_path_to_vfs_path(
123        &self,
124        storage: &dyn Storage,
125        querier: &QuerierWrapper,
126        vfs_contract: impl Into<String>,
127    ) -> Result<AndrAddr, ContractError> {
128        match self.is_local_path() {
129            true => {
130                let app_contract = ADOContract::default().get_app_contract(storage)?;
131                match app_contract {
132                    None => Err(ContractError::AppContractNotSpecified {}),
133                    Some(app_contract) => {
134                        let replaced = AndrAddr(self.0.replace("./", &format!("~{app_contract}/")));
135                        vfs_resolve_symlink(replaced, vfs_contract, querier)
136                    }
137                }
138            }
139            false => Ok(self.clone()),
140        }
141    }
142
143    /// Whether the provided address is local to the app
144    pub fn is_local_path(&self) -> bool {
145        self.0.starts_with("./")
146    }
147
148    /// Whether the provided address is a VFS path
149    pub fn is_vfs_path(&self) -> bool {
150        self.is_local_path()
151            || self.0.starts_with('/')
152            || self.0.split("://").count() > 1
153            || self.0.split('/').count() > 1
154            || self.0.starts_with('~')
155    }
156
157    /// Whether the provided address is a valid human readable address
158    pub fn is_addr(&self, api: &dyn Api) -> bool {
159        api.addr_validate(&self.0).is_ok()
160    }
161
162    /// Gets the chain for a given AndrAddr if it exists
163    ///
164    /// E.g. `ibc://cosmoshub-4/user/app/component` would return `cosmoshub-4`
165    ///
166    /// A human readable address will always return `None`
167    pub fn get_chain(&self) -> Option<&str> {
168        match self.get_protocol() {
169            None => None,
170            Some(..) => {
171                let start = self.0.find("://").unwrap() + 3;
172                let end = self.0[start..]
173                    .find('/')
174                    .unwrap_or_else(|| self.0[start..].len());
175                Some(&self.0[start..start + end])
176            }
177        }
178    }
179
180    /// Gets the protocol for a given AndrAddr if it exists
181    ///
182    /// E.g. `ibc://cosmoshub-4/user/app/component` would return `ibc`
183    ///
184    /// A human readable address will always return `None`
185    pub fn get_protocol(&self) -> Option<&str> {
186        if !self.is_vfs_path() {
187            None
188        } else {
189            let mut split = self.0.split("://");
190            if split.clone().count() == 1 {
191                None
192            } else {
193                Some(split.next().unwrap())
194            }
195        }
196    }
197
198    /// Gets the raw path for a given AndrAddr by stripping away any protocols or chain declarations.
199    ///
200    /// E.g. `ibc://cosmoshub-4/user/app/component` would return `/user/app/component`
201    ///
202    /// Returns the human readable address if the address is not a VFS path.
203    pub fn get_raw_path(&self) -> &str {
204        if !self.is_vfs_path() {
205            self.0.as_str()
206        } else {
207            match self.get_protocol() {
208                None => self.0.as_str(),
209                Some(..) => {
210                    let start = self.0.find("://").unwrap() + 3;
211                    let end = self.0[start..]
212                        .find('/')
213                        .unwrap_or_else(|| self.0[start..].len());
214                    &self.0[start + end..]
215                }
216            }
217        }
218    }
219
220    /// Gets the root directory for a given AndrAddr
221    ///
222    /// E.g. `/home/user/app/component` would return `home`
223    ///
224    /// Returns the human readable address if the address is not a VFS path or the local path if the address is a local reference
225    pub fn get_root_dir(&self) -> &str {
226        match self.is_vfs_path() {
227            false => self.0.as_str(),
228            true => match self.is_local_path() {
229                true => self.0.as_str(),
230                false => {
231                    let raw_path = self.get_raw_path();
232                    if raw_path.starts_with('~') {
233                        return "home";
234                    }
235                    raw_path.split('/').nth(1).unwrap()
236                }
237            },
238        }
239    }
240}
241
242impl Display for AndrAddr {
243    fn fmt(&self, f: &mut Formatter) -> FMTResult {
244        write!(f, "{}", &self.0)
245    }
246}
247
248impl AsRef<str> for AndrAddr {
249    #[inline]
250    fn as_ref(&self) -> &str {
251        self.as_str()
252    }
253}
254
255impl PartialEq<&str> for AndrAddr {
256    fn eq(&self, rhs: &&str) -> bool {
257        self.0 == *rhs
258    }
259}
260
261impl PartialEq<AndrAddr> for &str {
262    fn eq(&self, rhs: &AndrAddr) -> bool {
263        *self == rhs.0
264    }
265}
266
267impl PartialEq<String> for AndrAddr {
268    fn eq(&self, rhs: &String) -> bool {
269        &self.0 == rhs
270    }
271}
272
273impl PartialEq<AndrAddr> for String {
274    fn eq(&self, rhs: &AndrAddr) -> bool {
275        self == &rhs.0
276    }
277}
278
279impl From<AndrAddr> for String {
280    fn from(addr: AndrAddr) -> Self {
281        addr.0
282    }
283}
284
285impl From<&AndrAddr> for String {
286    fn from(addr: &AndrAddr) -> Self {
287        addr.0.clone()
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use cosmwasm_std::testing::mock_dependencies;
294    use regex::Regex;
295
296    use super::*;
297    struct ValidateRegexTestCase {
298        name: &'static str,
299        input: &'static str,
300        should_err: bool,
301    }
302
303    #[test]
304    fn test_validate() {
305        let deps = mock_dependencies();
306        let addr = AndrAddr("cosmos1...".to_string());
307        assert!(addr.validate(&deps.api).is_ok());
308
309        let addr = AndrAddr("ibc://cosmoshub-4/home/user/app/component".to_string());
310        assert!(addr.validate(&deps.api).is_ok());
311
312        let addr = AndrAddr("/home/user/app/component".to_string());
313        assert!(addr.validate(&deps.api).is_ok());
314
315        let addr = AndrAddr("./user/app/component".to_string());
316        assert!(addr.validate(&deps.api).is_ok());
317
318        let addr = AndrAddr("1".to_string());
319        assert!(addr.validate(&deps.api).is_err());
320    }
321
322    #[test]
323    fn test_is_vfs() {
324        let addr = AndrAddr("/home/user/app/component".to_string());
325        assert!(addr.is_vfs_path());
326
327        let addr = AndrAddr("./user/app/component".to_string());
328        assert!(addr.is_vfs_path());
329
330        let addr = AndrAddr("ibc://chain/home/user/app/component".to_string());
331        assert!(addr.is_vfs_path());
332
333        let addr = AndrAddr("cosmos1...".to_string());
334        assert!(!addr.is_vfs_path());
335    }
336
337    #[test]
338    fn test_is_addr() {
339        let deps = mock_dependencies();
340        let addr = AndrAddr("cosmos1...".to_string());
341        assert!(addr.is_addr(&deps.api));
342        assert!(!addr.is_vfs_path());
343    }
344
345    #[test]
346    fn test_is_local_path() {
347        let addr = AndrAddr("./component".to_string());
348        assert!(addr.is_local_path());
349        assert!(addr.is_vfs_path());
350    }
351
352    #[test]
353    fn test_get_protocol() {
354        let addr = AndrAddr("cosmos1...".to_string());
355        assert!(addr.get_protocol().is_none());
356
357        let addr = AndrAddr("ibc://chain/home/user/app/component".to_string());
358        assert_eq!(addr.get_protocol().unwrap(), "ibc");
359    }
360
361    #[test]
362    fn test_get_chain() {
363        let addr = AndrAddr("cosmos1...".to_string());
364        assert!(addr.get_chain().is_none());
365
366        let addr = AndrAddr("ibc://chain/home/user/app/component".to_string());
367        assert_eq!(addr.get_chain().unwrap(), "chain");
368
369        let addr = AndrAddr("/home/user/app/component".to_string());
370        assert!(addr.get_chain().is_none());
371    }
372
373    #[test]
374    fn test_get_raw_path() {
375        let addr = AndrAddr("cosmos1...".to_string());
376        assert_eq!(addr.get_raw_path(), "cosmos1...");
377
378        let addr = AndrAddr("ibc://chain/home/app/component".to_string());
379        assert_eq!(addr.get_raw_path(), "/home/app/component");
380
381        let addr = AndrAddr("/chain/home/app/component".to_string());
382        assert_eq!(addr.get_raw_path(), "/chain/home/app/component");
383    }
384
385    #[test]
386    fn test_get_root_dir() {
387        let addr = AndrAddr("/home/user1".to_string());
388        assert_eq!(addr.get_root_dir(), "home");
389
390        let addr = AndrAddr("~user1".to_string());
391        assert_eq!(addr.get_root_dir(), "home");
392
393        let addr = AndrAddr("~/user1".to_string());
394        assert_eq!(addr.get_root_dir(), "home");
395
396        let addr = AndrAddr("ibc://chain/home/user1".to_string());
397        assert_eq!(addr.get_root_dir(), "home");
398
399        let addr = AndrAddr("cosmos1...".to_string());
400        assert_eq!(addr.get_root_dir(), "cosmos1...");
401
402        let addr = AndrAddr("./home/user1".to_string());
403        assert_eq!(addr.get_root_dir(), "./home/user1");
404    }
405
406    #[test]
407    fn test_schemars_regex() {
408        let test_cases: Vec<ValidateRegexTestCase> = vec![
409            ValidateRegexTestCase {
410                name: "Normal Path",
411                input: "/home/user",
412                should_err: false,
413            },
414            ValidateRegexTestCase {
415                name: "Path with tilde",
416                input: "~user/dir",
417                should_err: false,
418            },
419            ValidateRegexTestCase {
420                name: "Wrong path with tilde",
421                input: "~/user/dir",
422                should_err: true,
423            },
424            ValidateRegexTestCase {
425                name: "Valid protocol",
426                input: "ibc://chain/home/user/dir",
427                should_err: false,
428            },
429            ValidateRegexTestCase {
430                name: "Valid protocol with tilde",
431                input: "ibc://chain/~user/dir",
432                should_err: false,
433            },
434            ValidateRegexTestCase {
435                name: "Valid Raw Address",
436                input: "cosmos1234567",
437                should_err: false,
438            },
439            ValidateRegexTestCase {
440                name: "Valid Local",
441                input: "./dir/file",
442                should_err: false,
443            },
444            ValidateRegexTestCase {
445                name: "Invalid Local",
446                input: "../dir/file",
447                should_err: true,
448            },
449        ];
450        let re = Regex::new(&ANDR_ADDR_REGEX).unwrap();
451        for test in test_cases {
452            let res = re.is_match(test.input);
453            assert_eq!(!res, test.should_err, "Test case: {}", test.name);
454        }
455    }
456}