1use derive_more::{Deref, DerefMut};
2use eyre::Report;
3use serde::{Deserializer, Serialize, Serializer};
4
5#[derive(Clone, Default, Copy, PartialEq, Eq, Hash, Deref, DerefMut, PartialOrd, Ord)]
6pub struct Asset(pub [u8; 16]);
7impl Asset {
8 pub fn new<S: AsRef<str>>(s: S) -> Self {
9 let s = s.as_ref().to_uppercase();
10 let mut bytes = [0; 16];
11 bytes[..s.len()].copy_from_slice(s.as_bytes());
12 Self(bytes)
13 }
14
15 fn fmt(&self) -> &str {
16 std::str::from_utf8(&self.0).unwrap().trim_end_matches('\0')
17 }
18
19 pub fn as_str(&self) -> &str {
20 self.fmt()
21 }
22}
23impl std::fmt::Display for Asset {
25 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
26 write!(f, "{}", self.fmt())
27 }
28}
29impl std::fmt::Debug for Asset {
30 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
31 write!(f, "{}", self.fmt())
32 }
33}
34impl From<&str> for Asset {
35 fn from(s: &str) -> Self {
36 Self::new(s)
37 }
38}
39impl From<String> for Asset {
40 fn from(s: String) -> Self {
41 Self::new(s)
42 }
43}
44impl AsRef<str> for Asset {
45 fn as_ref(&self) -> &str {
46 self.fmt()
47 }
48}
49impl PartialEq<str> for Asset {
50 fn eq(&self, other: &str) -> bool {
51 self.fmt() == other
52 }
53}
54impl PartialEq<&str> for Asset {
55 fn eq(&self, other: &&str) -> bool {
56 &self.fmt() == other
57 }
58}
59
60#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
61pub struct Pair {
62 base: Asset,
63 quote: Asset,
64}
65impl Pair {
66 pub fn new<S: Into<Asset>>(base: S, quote: S) -> Self {
67 Self {
68 base: base.into(),
69 quote: quote.into(),
70 }
71 }
72
73 pub fn is_usdt(&self) -> bool {
74 self.quote == "USDT" && self.base != "BTCST" }
76
77 pub fn base(&self) -> &Asset {
78 &self.base
79 }
80
81 pub fn quote(&self) -> &Asset {
82 &self.quote
83 }
84
85 pub fn fmt_binance(&self) -> String {
87 format!("{}{}", self.base, self.quote)
88 }
89
90 pub fn fmt_bybit(&self) -> String {
91 format!("{}{}", self.base, self.quote)
92 }
93
94 pub fn fmt_mexc(&self) -> String {
95 format!("{}_{}", self.base, self.quote)
96 }
97 }
99impl<A: Into<Asset>> From<(A, A)> for Pair {
100 fn from((base, quote): (A, A)) -> Self {
101 Self::new(base, quote)
102 }
103}
104impl std::fmt::Display for Pair {
106 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
107 write!(f, "{}-{}", self.base, self.quote)
108 }
109}
110
111impl<'de> serde::Deserialize<'de> for Pair {
112 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
113 where
114 D: Deserializer<'de>, {
115 let s = String::deserialize(deserializer)?;
116 s.parse().map_err(serde::de::Error::custom)
117 }
118}
119impl Serialize for Pair {
120 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
121 where
122 S: Serializer, {
123 self.to_string().serialize(serializer)
124 }
125}
126
127#[derive(thiserror::Error, Debug)]
128#[error("Invalid pair format '{provided_str}'. Expected two assets separated by one of: [{}]", allowed_delimiters.join(" "))]
129pub struct InvalidPairError {
130 provided_str: String,
131 allowed_delimiters: Vec<String>,
132}
133impl InvalidPairError {
134 pub fn new<S: Into<String>>(provided_str: &str, allowed_delimiters: impl IntoIterator<Item = S>) -> Self {
135 Self {
136 provided_str: provided_str.to_owned(),
137 allowed_delimiters: allowed_delimiters.into_iter().map(Into::into).collect(),
138 }
139 }
140}
141
142#[doc(hidden)]
145fn check_prefix_order<const N: usize>(arr: [&str; N]) -> eyre::Result<()> {
146 for i in 0..N {
147 for j in (i + 1)..N {
148 if arr[i].len() < arr[j].len() && arr[j].ends_with(arr[i]) {
149 eyre::bail!("{} is a suffix of {}", arr[i], arr[j]);
150 }
151 }
152 }
153 Ok(())
154}
155
156impl std::str::FromStr for Pair {
157 type Err = Report;
158
159 fn from_str(s: &str) -> Result<Self, Self::Err> {
160 let delimiters = [',', '-', '_', '/'];
161 let currencies = [
162 "EURI", "EUR", "USD", "GBP", "USDP", "USDS", "PLN", "RON", "CZK", "TRY", "JPY", "BRL", "RUB", "AUD", "NGN", "MXN", "COP", "ARS", "BKRW", "IDRT", "UAH", "BIDR", "BVND", "ZAR",
163 ];
164 let crypto = ["USDT", "USDC", "UST", "BTC", "WETH", "ETH", "BNB", "SOL", "XRP", "PAX", "DAI", "VAI", "DOGE", "DOT", "TRX"];
165 if let Err(e) = check_prefix_order(currencies) {
166 unreachable!("Invalid prefix order, I messed up bad: {e}");
167 }
168 if let Err(e) = check_prefix_order(crypto) {
169 unreachable!("Invalid prefix order, I messed up bad: {e}");
170 }
171 let recognized_quotes = [currencies.as_slice(), crypto.as_slice()].concat();
172
173 for delimiter in delimiters {
174 if s.contains(delimiter) {
175 let parts: Vec<_> = s.split(delimiter).map(str::trim).filter(|s| !s.is_empty()).collect();
176 if parts.len() == 2 {
177 return Ok(Self::new(parts[0], parts[1]));
178 }
179 return Err(InvalidPairError::new(s, delimiters.iter().map(|c| c.to_string())).into());
180 }
181 }
182
183 if let Some(quote) = recognized_quotes.iter().find(|q| s.ends_with(*q)) {
184 let base_len = s.len() - quote.len();
185 if base_len > 0 {
186 let base = &s[..base_len];
187 return Ok(Self::new(base, *quote));
188 }
189 }
190
191 Err(InvalidPairError::new(s, delimiters.iter().map(|c| c.to_string())).into())
192 }
193}
194impl TryFrom<&str> for Pair {
195 type Error = Report;
196
197 fn try_from(s: &str) -> Result<Self, Self::Error> {
198 s.parse()
199 }
200}
201impl TryFrom<String> for Pair {
202 type Error = Report;
203
204 fn try_from(s: String) -> Result<Self, Self::Error> {
205 s.parse()
206 }
207}
208impl From<Pair> for String {
209 fn from(pair: Pair) -> Self {
210 pair.to_string()
211 }
212}
213
214impl PartialEq<Pair> for &str {
215 fn eq(&self, other: &Pair) -> bool {
216 Pair::try_from(*self).expect("provided string can't be converted to `Pair` automatically") == *other }
218}
219
220impl PartialEq<Pair> for str {
221 fn eq(&self, other: &Pair) -> bool {
222 self == other
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn parse_pairs() {
232 assert_eq!("BTC-USD".parse::<Pair>().unwrap(), Pair::new("BTC", "USD"));
233 assert_eq!("ETH,USD".parse::<Pair>().unwrap(), Pair::new("ETH", "USD"));
234 assert_eq!("SOL_USDT".parse::<Pair>().unwrap(), Pair::new("SOL", "USDT"));
235 assert_eq!("XRP/USDC".parse::<Pair>().unwrap(), Pair::new("XRP", "USDC"));
236 assert_eq!("btc - usd".parse::<Pair>().unwrap(), Pair::new("BTC", "USD"));
237 assert_eq!("DOGEUSDT".parse::<Pair>().unwrap(), Pair::new("DOGE", "USDT"));
238 assert_eq!(Pair::from(("ADA", "USDT")), Pair::new("ADA", "USDT"));
239
240 assert!("something".parse::<Pair>().is_err());
241 assert!("".parse::<Pair>().is_err());
242 assert!("BTC".parse::<Pair>().is_err());
243 assert!("BTC-".parse::<Pair>().is_err());
244 assert!("-USD".parse::<Pair>().is_err());
245 }
246
247 #[test]
248 fn display_pairs() {
249 assert_eq!(Pair::new("BTC", "USDT").to_string(), "BTC-USDT");
250 }
251}