nautilus_model/defi/
token.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{
17    fmt::{Display, Formatter},
18    sync::Arc,
19};
20
21use alloy_primitives::Address;
22use serde::{Deserialize, Serialize};
23
24use crate::defi::chain::SharedChain;
25
26/// Represents a cryptocurrency token on a blockchain network.
27#[cfg_attr(
28    feature = "python",
29    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
30)]
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub struct Token {
33    /// The blockchain network where this token exists.
34    pub chain: SharedChain,
35    /// The blockchain address of the token contract.
36    pub address: Address,
37    /// The full name of the token.
38    pub name: String,
39    /// The token's ticker symbol.
40    pub symbol: String,
41    /// The number of decimal places used to represent fractional token amounts.
42    pub decimals: u8,
43}
44
45/// A thread-safe shared pointer to a `Token`, enabling efficient reuse across multiple components.
46pub type SharedToken = Arc<Token>;
47
48impl Token {
49    /// Creates a new [`Token`] instance with the specified properties.
50    #[must_use]
51    pub fn new(
52        chain: SharedChain,
53        address: Address,
54        name: String,
55        symbol: String,
56        decimals: u8,
57    ) -> Self {
58        Self {
59            chain,
60            address,
61            name,
62            symbol,
63            decimals,
64        }
65    }
66
67    /// Returns true if this token is a stablecoin.
68    ///
69    /// Checks against common stablecoin symbols including USD-pegged tokens,
70    /// Euro-pegged tokens, and other algorithmic/collateralized stablecoins.
71    pub fn is_stablecoin(&self) -> bool {
72        matches!(
73            self.symbol.as_str(),
74            "USDC"
75                | "USDT"
76                | "DAI"
77                | "BUSD"
78                | "FRAX"
79                | "LUSD"
80                | "TUSD"
81                | "USDP"
82                | "GUSD"
83                | "SUSD"
84                | "UST"
85                | "USDD"
86                | "CUSD"
87                | "EUROC"
88                | "EURT"
89                | "EURS"
90                | "AGEUR"
91                | "MIM"
92                | "FEI"
93                | "OUSD"
94                | "USDB"
95        )
96    }
97
98    /// Returns true if this token is a native blockchain currency wrapper.
99    ///
100    /// Identifies wrapped versions of native currencies like WETH (Wrapped ETH),
101    /// WMATIC (Wrapped MATIC), WBNB (Wrapped BNB), etc.
102    pub fn is_native_currency(&self) -> bool {
103        matches!(
104            self.symbol.as_str(),
105            "WETH"
106                | "ETH"
107                | "WMATIC"
108                | "MATIC"
109                | "WBNB"
110                | "BNB"
111                | "WAVAX"
112                | "AVAX"
113                | "WFTM"
114                | "FTM"
115        )
116    }
117
118    /// Returns the priority of this token for base/quote determination.
119    ///
120    /// Lower numbers indicate higher priority to become the quote token (pricing currency).
121    /// This follows market conventions where trades are quoted in the most liquid/stable assets.
122    ///
123    /// # Priority Levels
124    /// - **1**: Stablecoins (USDC, USDT, DAI, etc.) - Highest priority to be quote
125    /// - **2**: Native currencies (WETH, WMATIC, WBNB, etc.) - Medium priority
126    /// - **3**: Other tokens - Lowest priority (typically become base tokens)
127    pub fn get_token_priority(&self) -> u8 {
128        if self.is_stablecoin() {
129            1
130        } else if self.is_native_currency() {
131            2
132        } else {
133            3
134        }
135    }
136}
137
138impl Display for Token {
139    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
140        write!(f, "Token(symbol={}, name={})", self.symbol, self.name)
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use std::sync::Arc;
147
148    use rstest::rstest;
149
150    use super::*;
151    use crate::defi::{chain::chains, stubs::weth};
152
153    #[rstest]
154    fn test_token_constructor(weth: Token) {
155        assert_eq!(weth.chain.chain_id, chains::ARBITRUM.chain_id);
156        assert_eq!(weth.name, "Wrapped Ether");
157        assert_eq!(weth.symbol, "WETH");
158        assert_eq!(weth.decimals, 18);
159        assert!(weth.is_native_currency());
160    }
161
162    #[rstest]
163    fn test_token_display_with_special_characters() {
164        // Test edge case where token names/symbols contain formatting characters
165        let chain = Arc::new(chains::ETHEREUM.clone());
166        let token = Token::new(
167            chain,
168            "0xA0b86a33E6441b936662bb6B5d1F8Fb0E2b57A5D"
169                .parse()
170                .unwrap(),
171            "Test Token (with parentheses)".to_string(),
172            "TEST-1".to_string(),
173            18,
174        );
175
176        let display = token.to_string();
177        assert_eq!(
178            display,
179            "Token(symbol=TEST-1, name=Test Token (with parentheses))"
180        );
181        assert!(!token.is_native_currency());
182        assert!(!token.is_stablecoin());
183        assert_eq!(token.get_token_priority(), 3);
184    }
185}