1use cosmwasm_std::{StdError, StdResult};
3use cw_storage_plus::{Key, KeyDeserialize, Prefixer, PrimaryKey};
4use schemars::{
5 schema::{InstanceType, SchemaObject},
6 JsonSchema,
7};
8use serde::de::Visitor;
9
10use crate::prelude::*;
11
12#[cw_serde]
14#[derive(Eq, Hash, Copy)]
15pub enum MarketType {
16 CollateralIsQuote,
18 CollateralIsBase,
20}
21
22#[derive(Clone)]
24pub struct MarketId {
25 base: String,
26 quote: String,
27 market_type: MarketType,
28 encoded: String,
29}
30
31impl std::hash::Hash for MarketId {
32 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
33 self.encoded.hash(state);
34 }
35}
36
37impl PartialEq for MarketId {
38 fn eq(&self, other: &Self) -> bool {
39 self.encoded == other.encoded
40 }
41}
42
43impl Eq for MarketId {}
44
45#[allow(clippy::non_canonical_partial_ord_impl)]
46impl PartialOrd for MarketId {
47 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
48 self.encoded.partial_cmp(&other.encoded)
49 }
50}
51
52impl Ord for MarketId {
53 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
54 self.encoded.cmp(&other.encoded)
55 }
56}
57
58impl std::fmt::Debug for MarketId {
59 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
60 std::fmt::Debug::fmt(&self.encoded, f)
61 }
62}
63
64fn is_fiat(s: &str) -> bool {
67 s == "USD" || s == "EUR"
69}
70
71fn make_encoded(base: &str, quote: &str, market_type: MarketType) -> String {
72 let (base_plus, quote_plus) = match (market_type, is_fiat(quote)) {
73 (MarketType::CollateralIsQuote, true) => ("", "+"),
75 (MarketType::CollateralIsQuote, false) => ("", ""),
77 (MarketType::CollateralIsBase, true) => ("", ""),
79 (MarketType::CollateralIsBase, false) => ("+", ""),
81 };
82 format!("{base}{base_plus}_{quote}{quote_plus}")
83}
84
85impl MarketId {
86 pub fn new(base: impl Into<String>, quote: impl Into<String>, market_type: MarketType) -> Self {
88 let base = base.into();
89 let quote = quote.into();
90 let encoded = make_encoded(&base, "e, market_type);
91 MarketId {
92 base,
93 quote,
94 market_type,
95 encoded,
96 }
97 }
98
99 pub fn is_notional_usd(&self) -> bool {
103 self.get_notional() == "USD"
104 }
105
106 pub fn as_str(&self) -> &str {
108 &self.encoded
109 }
110
111 fn parse(s: &str) -> Option<Self> {
112 let (base, quote) = s.split_once('_')?;
113 let (base, base_is_collateral) = match base.strip_suffix('+') {
114 Some(base) => {
115 if is_fiat(quote) {
116 return None;
117 } else {
118 (base, true)
119 }
120 }
121 None => (base, false),
122 };
123 let (quote, quote_is_collateral) = match quote.strip_suffix('+') {
124 Some(quote) => {
125 if is_fiat(quote) {
126 (quote, true)
127 } else {
128 return None;
129 }
130 }
131 None => (quote, false),
132 };
133 let market_type = match (base_is_collateral, quote_is_collateral) {
134 (true, true) => return None,
135 (true, false) => MarketType::CollateralIsBase,
136 (false, true) => MarketType::CollateralIsQuote,
137 (false, false) => {
138 if is_fiat(quote) {
139 MarketType::CollateralIsBase
140 } else {
141 MarketType::CollateralIsQuote
142 }
143 }
144 };
145
146 assert_eq!(make_encoded(base, quote, market_type), s);
147 Some(MarketId {
148 base: base.to_owned(),
149 quote: quote.to_owned(),
150 market_type,
151 encoded: s.to_owned(),
152 })
153 }
154
155 pub fn get_notional(&self) -> &str {
157 match self.market_type {
158 MarketType::CollateralIsQuote => &self.base,
159 MarketType::CollateralIsBase => &self.quote,
160 }
161 }
162
163 pub fn get_collateral(&self) -> &str {
165 match self.market_type {
166 MarketType::CollateralIsQuote => &self.quote,
167 MarketType::CollateralIsBase => &self.base,
168 }
169 }
170
171 pub fn get_base(&self) -> &str {
173 &self.base
174 }
175
176 pub fn get_quote(&self) -> &str {
178 &self.quote
179 }
180
181 pub fn get_market_type(&self) -> MarketType {
183 self.market_type
184 }
185}
186
187impl Display for MarketId {
188 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
189 f.write_str(&self.encoded)
190 }
191}
192
193impl FromStr for MarketId {
194 type Err = StdError;
195
196 fn from_str(s: &str) -> Result<Self, Self::Err> {
197 MarketId::parse(s)
198 .ok_or_else(|| StdError::parse_err("MarketId", format!("Invalid market ID: {s}")))
199 }
200}
201
202impl serde::Serialize for MarketId {
203 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
204 where
205 S: serde::Serializer,
206 {
207 serializer.serialize_str(&self.encoded)
208 }
209}
210
211impl<'de> serde::Deserialize<'de> for MarketId {
212 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
213 where
214 D: serde::Deserializer<'de>,
215 {
216 deserializer.deserialize_str(MarketIdVisitor)
217 }
218}
219
220struct MarketIdVisitor;
221
222impl<'de> Visitor<'de> for MarketIdVisitor {
223 type Value = MarketId;
224
225 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
226 formatter.write_str("MarketId")
227 }
228
229 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
230 where
231 E: serde::de::Error,
232 {
233 MarketId::parse(v).ok_or_else(|| E::custom(format!("Invalid market ID: {v}")))
234 }
235}
236
237impl JsonSchema for MarketId {
238 fn schema_name() -> String {
239 "MarketId".to_owned()
240 }
241
242 fn json_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
243 SchemaObject {
244 instance_type: Some(InstanceType::String.into()),
245 format: Some("market-id".to_owned()),
246 ..Default::default()
247 }
248 .into()
249 }
250}
251
252impl<'a> PrimaryKey<'a> for MarketId {
253 type Prefix = ();
254 type SubPrefix = ();
255 type Suffix = Self;
256 type SuperSuffix = Self;
257
258 fn key(&self) -> Vec<Key> {
259 let key = Key::Ref(self.encoded.as_bytes());
260
261 vec![key]
262 }
263}
264
265impl<'a> Prefixer<'a> for MarketId {
266 fn prefix(&self) -> Vec<Key> {
267 let key = Key::Ref(self.encoded.as_bytes());
268 vec![key]
269 }
270}
271
272impl KeyDeserialize for MarketId {
273 type Output = MarketId;
274
275 const KEY_ELEMS: u16 = 1;
276
277 fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
278 std::str::from_utf8(&value)
279 .map_err(StdError::invalid_utf8)
280 .and_then(|s| s.parse())
281 }
282}
283
284impl KeyDeserialize for &'_ MarketId {
285 type Output = MarketId;
286
287 const KEY_ELEMS: u16 = 1;
288
289 fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
290 std::str::from_utf8(&value)
291 .map_err(StdError::invalid_utf8)
292 .and_then(|s| s.parse())
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn round_trip_usd_as_collateral() {
302 let orig = MarketId::new("BTC", "USD", MarketType::CollateralIsQuote);
303 assert_eq!(orig.as_str(), "BTC_USD+");
304 let parsed: MarketId = "BTC_USD+".parse().unwrap();
305 assert_eq!(orig, parsed);
306
307 assert_eq!(orig.get_base(), "BTC");
308 assert_eq!(orig.get_quote(), "USD");
309 assert_eq!(orig.get_notional(), "BTC");
310 assert_eq!(orig.get_collateral(), "USD");
311 }
312
313 #[test]
314 fn round_trip_usd_as_notional() {
315 let orig = MarketId::new("BTC", "USD", MarketType::CollateralIsBase);
316 assert_eq!(orig.as_str(), "BTC_USD");
317 let parsed: MarketId = "BTC_USD".parse().unwrap();
318 assert_eq!(orig, parsed);
319
320 assert_eq!(orig.get_base(), "BTC");
321 assert_eq!(orig.get_quote(), "USD");
322 assert_eq!(orig.get_notional(), "USD");
323 assert_eq!(orig.get_collateral(), "BTC");
324 }
325
326 #[test]
327 fn round_trip_usdc_as_collateral() {
328 let orig = MarketId::new("BTC", "USDC", MarketType::CollateralIsQuote);
329 assert_eq!(orig.as_str(), "BTC_USDC");
330 let parsed: MarketId = "BTC_USDC".parse().unwrap();
331 assert_eq!(orig, parsed);
332
333 assert_eq!(orig.get_base(), "BTC");
334 assert_eq!(orig.get_quote(), "USDC");
335 assert_eq!(orig.get_notional(), "BTC");
336 assert_eq!(orig.get_collateral(), "USDC");
337 }
338
339 #[test]
340 fn round_trip_usdc_as_notional() {
341 let orig = MarketId::new("BTC", "USDC", MarketType::CollateralIsBase);
342 assert_eq!(orig.as_str(), "BTC+_USDC");
343 let parsed: MarketId = "BTC+_USDC".parse().unwrap();
344 assert_eq!(orig, parsed);
345
346 assert_eq!(orig.get_base(), "BTC");
347 assert_eq!(orig.get_quote(), "USDC");
348 assert_eq!(orig.get_notional(), "USDC");
349 assert_eq!(orig.get_collateral(), "BTC");
350 }
351
352 #[test]
353 fn no_unnecessary_plus() {
354 MarketId::from_str("BTC+_USD").unwrap_err();
355 MarketId::from_str("BTC_USDC+").unwrap_err();
356 assert_eq!(
357 MarketId::from_str("BTC_USD").unwrap(),
358 MarketId::new("BTC", "USD", MarketType::CollateralIsBase)
359 );
360 assert_eq!(
361 MarketId::from_str("BTC_USD+").unwrap(),
362 MarketId::new("BTC", "USD", MarketType::CollateralIsQuote)
363 );
364 assert_eq!(
365 MarketId::from_str("BTC_USDC").unwrap(),
366 MarketId::new("BTC", "USDC", MarketType::CollateralIsQuote)
367 );
368 assert_eq!(
369 MarketId::from_str("BTC+_USDC").unwrap(),
370 MarketId::new("BTC", "USDC", MarketType::CollateralIsBase)
371 );
372 }
373
374 #[test]
375 fn round_trip_market_id_collateral_is_quote() {
376 let orig = MarketId::new("BTC", "USD", MarketType::CollateralIsQuote);
377 assert_eq!(orig.as_str(), "BTC_USD+");
378 let parsed: MarketId = "BTC_USD+".parse().unwrap();
379 assert_eq!(orig, parsed);
380
381 assert_eq!(orig.get_base(), "BTC");
382 assert_eq!(orig.get_quote(), "USD");
383 assert_eq!(orig.get_notional(), "BTC");
384 assert_eq!(orig.get_collateral(), "USD");
385 }
386
387 #[test]
388 fn round_trip_market_id_usd_collateral_is_base() {
389 let orig = MarketId::new("BTC", "USD", MarketType::CollateralIsBase);
390 assert_eq!(orig.as_str(), "BTC_USD");
391 let parsed: MarketId = "BTC_USD".parse().unwrap();
392 assert_eq!(orig, parsed);
393
394 assert_eq!(orig.get_base(), "BTC");
395 assert_eq!(orig.get_quote(), "USD");
396 assert_eq!(orig.get_notional(), "USD");
397 assert_eq!(orig.get_collateral(), "BTC");
398 }
399
400 #[test]
401 fn notional_to_usd() {
402 let market_id = MarketId::from_str("ATOM_OSMO").unwrap();
404
405 let price_usd = PriceCollateralInUsd::from_str("2").unwrap();
407
408 let price_base = PriceBaseInQuote::from_str("5").unwrap();
410
411 let price_point = PricePoint {
412 price_notional: price_base.into_notional_price(market_id.get_market_type()),
413 price_usd,
414 price_base,
415 timestamp: Default::default(),
416 is_notional_usd: market_id.is_notional_usd(),
417 market_type: market_id.get_market_type(),
418 publish_time: None,
419 publish_time_usd: None,
420 };
421
422 let in_usd = price_point.notional_to_usd("50".parse().unwrap());
423
424 assert_eq!(in_usd, "500".parse().unwrap());
426 }
427}