Skip to main content

soroban_cli/config/
sc_address.rs

1use std::str::FromStr;
2
3use crate::xdr;
4
5use super::{alias, key, locator, UnresolvedContract};
6
7/// `ScAddress` can be either a resolved `xdr::ScAddress` or an alias of a `Contract` or `MuxedAccount`.
8#[allow(clippy::module_name_repetitions)]
9#[derive(Clone, Debug)]
10pub enum UnresolvedScAddress {
11    Resolved(xdr::ScAddress),
12    Alias(String),
13}
14
15impl Default for UnresolvedScAddress {
16    fn default() -> Self {
17        UnresolvedScAddress::Alias(String::default())
18    }
19}
20
21#[derive(thiserror::Error, Debug)]
22pub enum Error {
23    #[error(transparent)]
24    Locator(#[from] locator::Error),
25    #[error(transparent)]
26    Key(#[from] key::Error),
27    #[error("Account alias \"{0}\" not Found")]
28    AccountAliasNotFound(String),
29    #[error("alias '{0}' is reserved for the native asset contract but also matches a stored key; pass an explicit contract (C...) or account (G...) address instead")]
30    ReservedAliasShadowsKey(String),
31}
32
33impl FromStr for UnresolvedScAddress {
34    type Err = Error;
35
36    fn from_str(value: &str) -> Result<Self, Self::Err> {
37        Ok(xdr::ScAddress::from_str(value).map_or_else(
38            |_| UnresolvedScAddress::Alias(value.to_string()),
39            UnresolvedScAddress::Resolved,
40        ))
41    }
42}
43
44impl UnresolvedScAddress {
45    pub fn resolve(
46        self,
47        locator: &locator::Args,
48        network_passphrase: &str,
49        hd_path: Option<u32>,
50    ) -> Result<xdr::ScAddress, Error> {
51        let alias = match self {
52            UnresolvedScAddress::Resolved(addr) => return Ok(addr),
53            UnresolvedScAddress::Alias(alias) => alias,
54        };
55        let contract = UnresolvedContract::resolve_alias(&alias, locator, network_passphrase);
56        let key = locator.read_key(&alias);
57        match (contract, key) {
58            (Ok(contract), Ok(_)) => {
59                // A reserved built-in alias (e.g. `native`) shadows an on-disk
60                // key of the same name. Preferring either side could send funds
61                // to the wrong address, so refuse and ask for an explicit one.
62                if alias::is_reserved(&alias) {
63                    return Err(Error::ReservedAliasShadowsKey(alias));
64                }
65                eprintln!(
66                    "Warning: ScAddress alias {alias} is ambiguous, assuming it is a contract"
67                );
68                Ok(xdr::ScAddress::Contract(stellar_xdr::ContractId(
69                    xdr::Hash(contract.0),
70                )))
71            }
72            (Ok(contract), _) => Ok(xdr::ScAddress::Contract(stellar_xdr::ContractId(
73                xdr::Hash(contract.0),
74            ))),
75            // Surface a shadowed reserved-alias collision rather than masking it
76            // with a generic "not found" error. This must precede the key arm:
77            // when both a stored `native` alias and a `native` key exist, the
78            // collision has to win so resolution can't silently pick the key.
79            (Err(err @ locator::Error::ShadowedReservedAlias { .. }), _) => Err(err.into()),
80            (_, Ok(key)) => Ok(xdr::ScAddress::Account(
81                key.muxed_account(hd_path)?.account_id(),
82            )),
83            _ => Err(Error::AccountAliasNotFound(alias)),
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::config::key::Key;
92    use crate::config::locator::KeyType;
93    use std::str::FromStr;
94
95    #[test]
96    fn resolve_errors_when_reserved_alias_shadows_key() {
97        let dir = tempfile::tempdir().unwrap();
98        let locator = locator::Args {
99            config_dir: Some(dir.path().to_path_buf()),
100        };
101        let network_passphrase = "Test Network";
102        let native = alias::NATIVE;
103
104        // A key named after the native alias, created before it became
105        // reserved. Written directly since `write_identity` now rejects it.
106        let key =
107            Key::from_str("SBEQMTXGCLDFQG3OXMRSMGLKJCPROAHB5GZCCGVZERDI645LCCCRLFGY").unwrap();
108        KeyType::Identity.write(native, &key, dir.path()).unwrap();
109
110        let err = UnresolvedScAddress::Alias(native.to_string())
111            .resolve(&locator, network_passphrase, None)
112            .unwrap_err();
113
114        assert!(matches!(err, Error::ReservedAliasShadowsKey(alias) if alias == native));
115    }
116
117    #[test]
118    fn resolve_errors_when_reserved_alias_shadowed_by_stored_alias_and_key() {
119        let dir = tempfile::tempdir().unwrap();
120        let locator = locator::Args {
121            config_dir: Some(dir.path().to_path_buf()),
122        };
123        let network_passphrase = "Test Network";
124        let native = alias::NATIVE;
125
126        // Both pre-upgrade artifacts exist: a key named after the native alias
127        // and a stored alias of the same name pointing at another contract. The
128        // shadowed-alias error must win rather than the resolution silently
129        // picking the key.
130        let key =
131            Key::from_str("SBEQMTXGCLDFQG3OXMRSMGLKJCPROAHB5GZCCGVZERDI645LCCCRLFGY").unwrap();
132        KeyType::Identity.write(native, &key, dir.path()).unwrap();
133
134        let contract_ids = dir.path().join("contract-ids");
135        std::fs::create_dir_all(&contract_ids).unwrap();
136        std::fs::write(
137            contract_ids.join(format!("{native}.json")),
138            format!(
139                r#"{{"ids":{{"{network_passphrase}":"CA3D5KRYM6CB7OWQ6TWYRR3Z4T7GNZLKERYNZGGA5SOAOPIFY6YQGAXE"}}}}"#
140            ),
141        )
142        .unwrap();
143
144        let err = UnresolvedScAddress::Alias(native.to_string())
145            .resolve(&locator, network_passphrase, None)
146            .unwrap_err();
147
148        assert!(matches!(
149            err,
150            Error::Locator(locator::Error::ShadowedReservedAlias { alias, .. }) if alias == native
151        ));
152    }
153}