Skip to main content

soroban_cli/config/
token.rs

1use std::str::FromStr;
2
3use crate::{
4    config::{alias::UnresolvedContract, locator},
5    tx::builder,
6    utils::contract_id_hash_from_asset,
7};
8
9#[derive(thiserror::Error, Debug)]
10pub enum Error {
11    #[error(transparent)]
12    Locator(#[from] locator::Error),
13    #[error(transparent)]
14    Asset(#[from] builder::asset::Error),
15}
16
17impl Error {
18    /// Machine-readable discriminator for the JSON error envelope's `type` field.
19    #[must_use]
20    pub fn error_type(&self) -> &'static str {
21        match self {
22            Error::Locator(_) => "config",
23            Error::Asset(_) => "invalid_asset",
24        }
25    }
26}
27
28/// What a token reference resolved to.
29#[derive(Clone, Debug)]
30pub enum TokenKind {
31    /// A Stellar Asset Contract (SAC) wrapping this classic asset.
32    Sac(crate::xdr::Asset),
33    /// A plain SEP-41 contract addressed by id or alias.
34    Contract,
35}
36
37/// A token reference resolved to a concrete contract id, together with what it
38/// turned out to be. This is the single answer to both "what contract id?" and
39/// "is this a SAC, and for which asset?", so SAC-awareness (deploy hints,
40/// `sac_not_deployed` vs `contract_not_found` errors) stays consistent
41/// everywhere.
42#[derive(Clone, Debug)]
43pub struct ResolvedToken {
44    pub contract_id: stellar_strkey::Contract,
45    pub kind: TokenKind,
46}
47
48impl ResolvedToken {
49    /// Returns `true` if this token is a Stellar Asset Contract.
50    #[must_use]
51    pub fn is_sac(&self) -> bool {
52        matches!(self.kind, TokenKind::Sac(_))
53    }
54
55    /// The classic asset backing this token, or `None` for a plain contract.
56    #[must_use]
57    pub fn asset(&self) -> Option<&crate::xdr::Asset> {
58        match &self.kind {
59            TokenKind::Sac(asset) => Some(asset),
60            TokenKind::Contract => None,
61        }
62    }
63}
64
65/// An unresolved token reference parsed from a user-supplied string.
66///
67/// The shape of the value decides how it is interpreted:
68/// * `native` or `CODE:ISSUER` → a Stellar Asset Contract (SAC), resolved from
69///   the classic asset.
70/// * anything else → a contract, either a `C…` strkey or a saved alias.
71#[derive(Clone, Debug)]
72pub enum UnresolvedToken {
73    /// A Stellar Asset Contract addressed by its underlying classic asset.
74    Asset(builder::Asset),
75    /// A SEP-41 contract addressed directly by id or alias.
76    Contract(UnresolvedContract),
77}
78
79impl FromStr for UnresolvedToken {
80    type Err = builder::asset::Error;
81
82    fn from_str(value: &str) -> Result<Self, Self::Err> {
83        // `native` and the `CODE:ISSUER` shape are both classic assets, resolved
84        // to their Stellar Asset Contract — so a missing SAC reports
85        // `sac_not_deployed`, not `contract_not_found`. Everything else is a
86        // contract id or a saved alias.
87        if value == "native" || value.contains(':') {
88            Ok(UnresolvedToken::Asset(value.parse()?))
89        } else {
90            // `UnresolvedContract::from_str` is infallible.
91            Ok(UnresolvedToken::Contract(value.parse().unwrap()))
92        }
93    }
94}
95
96impl UnresolvedToken {
97    /// Resolve this reference to a concrete contract id and classify it as a SAC
98    /// (with its asset) or a plain contract.
99    pub fn resolve(
100        &self,
101        locator: &locator::Args,
102        network_passphrase: &str,
103    ) -> Result<ResolvedToken, Error> {
104        match self {
105            UnresolvedToken::Asset(asset) => {
106                let asset = asset.resolve(locator)?;
107                let contract_id = contract_id_hash_from_asset(&asset, network_passphrase);
108                Ok(ResolvedToken {
109                    contract_id,
110                    kind: TokenKind::Sac(asset),
111                })
112            }
113            UnresolvedToken::Contract(contract) => {
114                let contract_id = contract.resolve_contract_id(locator, network_passphrase)?;
115                Ok(ResolvedToken {
116                    contract_id,
117                    kind: TokenKind::Contract,
118                })
119            }
120        }
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    // A valid contract strkey borrowed from the CLI's own help text.
129    const CONTRACT: &str = "CCR6QKTWZQYW6YUJ7UP7XXZRLWQPFRV6SWBLQS4ZQOSAF4BOUD77OTE2";
130    const NETWORK: &str = "Test Network";
131
132    #[test]
133    fn native_parses_as_asset() {
134        assert!(matches!(
135            "native".parse::<UnresolvedToken>().unwrap(),
136            UnresolvedToken::Asset(builder::Asset::Native)
137        ));
138    }
139
140    #[test]
141    fn code_issuer_parses_as_asset() {
142        assert!(matches!(
143            "USDC:issuer".parse::<UnresolvedToken>().unwrap(),
144            UnresolvedToken::Asset(builder::Asset::Asset(..))
145        ));
146    }
147
148    #[test]
149    fn contract_strkey_parses_as_resolved_contract() {
150        assert!(matches!(
151            CONTRACT.parse::<UnresolvedToken>().unwrap(),
152            UnresolvedToken::Contract(UnresolvedContract::Resolved(_))
153        ));
154    }
155
156    #[test]
157    fn bare_name_parses_as_contract_alias() {
158        assert!(matches!(
159            "alice".parse::<UnresolvedToken>().unwrap(),
160            UnresolvedToken::Contract(UnresolvedContract::Alias(_))
161        ));
162    }
163
164    #[test]
165    fn native_resolves_to_sac() {
166        let locator = locator::Args::default();
167        let resolved = "native"
168            .parse::<UnresolvedToken>()
169            .unwrap()
170            .resolve(&locator, NETWORK)
171            .unwrap();
172
173        assert!(resolved.is_sac());
174        assert!(matches!(resolved.asset(), Some(crate::xdr::Asset::Native)));
175        assert_eq!(
176            resolved.contract_id,
177            contract_id_hash_from_asset(&crate::xdr::Asset::Native, NETWORK)
178        );
179    }
180
181    #[test]
182    fn code_issuer_resolves_to_sac() {
183        let locator = locator::Args::default();
184        // A `CODE:ISSUER` reference with a concrete G… issuer needs no alias
185        // lookup, so it resolves without a populated locator.
186        let resolved = "USDC:GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
187            .parse::<UnresolvedToken>()
188            .unwrap()
189            .resolve(&locator, NETWORK)
190            .unwrap();
191
192        assert!(resolved.is_sac());
193        assert!(matches!(
194            resolved.asset(),
195            Some(crate::xdr::Asset::CreditAlphanum4(_))
196        ));
197    }
198
199    #[test]
200    fn contract_strkey_resolves_to_contract() {
201        let locator = locator::Args::default();
202        let resolved = CONTRACT
203            .parse::<UnresolvedToken>()
204            .unwrap()
205            .resolve(&locator, NETWORK)
206            .unwrap();
207
208        assert!(!resolved.is_sac());
209        assert!(resolved.asset().is_none());
210        assert_eq!(resolved.contract_id, CONTRACT.parse().unwrap());
211    }
212}