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}