nautilus_model/defi/
token.rs

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