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}