1use rust_decimal::Decimal;
6use serde::{Deserialize, Deserializer, Serialize};
7use std::collections::{BTreeMap, HashMap};
8use std::str::FromStr;
9
10use crate::decimal::UnsignedDecimal;
11use crate::errors::O2Error;
12
13macro_rules! newtype_id {
14 ($(#[$meta:meta])* $name:ident) => {
15 $(#[$meta])*
16 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
17 #[serde(transparent)]
18 pub struct $name(String);
19
20 impl $name {
21 pub(crate) fn new(s: impl Into<String>) -> Self {
23 Self(s.into())
24 }
25
26 pub fn as_str(&self) -> &str {
28 &self.0
29 }
30 }
31
32 impl AsRef<str> for $name {
33 fn as_ref(&self) -> &str {
34 &self.0
35 }
36 }
37
38 impl std::fmt::Display for $name {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.write_str(&self.0)
41 }
42 }
43
44 impl From<&$name> for $name {
45 fn from(v: &$name) -> Self {
46 v.clone()
47 }
48 }
49
50 impl std::ops::Deref for $name {
51 type Target = str;
52 fn deref(&self) -> &str {
53 &self.0
54 }
55 }
56
57 impl Default for $name {
58 fn default() -> Self {
59 Self(String::new())
60 }
61 }
62 };
63}
64
65pub trait IntoValidId<T> {
68 fn into_valid(self) -> Result<T, O2Error>;
69}
70
71fn validate_hex(type_name: &str, s: &str) -> Result<(), O2Error> {
72 let hex = s
73 .strip_prefix("0x")
74 .or_else(|| s.strip_prefix("0X"))
75 .unwrap_or(s);
76 if hex.is_empty() {
77 return Err(O2Error::Other(format!(
78 "{type_name}: requires a non-empty hex string, got {s:?}"
79 )));
80 }
81 if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
82 return Err(O2Error::Other(format!(
83 "{type_name}: contains non-hex characters: {s:?}"
84 )));
85 }
86 Ok(())
87}
88
89macro_rules! hex_id {
92 ($(#[$meta:meta])* $name:ident) => {
93 newtype_id!($(#[$meta])* $name);
94
95 impl IntoValidId<$name> for &str {
96 fn into_valid(self) -> Result<$name, O2Error> {
97 validate_hex(stringify!($name), self)?;
98 Ok($name::new(self))
99 }
100 }
101
102 impl IntoValidId<$name> for String {
103 fn into_valid(self) -> Result<$name, O2Error> {
104 validate_hex(stringify!($name), &self)?;
105 Ok($name::new(self))
106 }
107 }
108
109 impl IntoValidId<$name> for $name {
110 fn into_valid(self) -> Result<$name, O2Error> {
111 Ok(self)
112 }
113 }
114
115 impl IntoValidId<$name> for &$name {
116 fn into_valid(self) -> Result<$name, O2Error> {
117 Ok(self.clone())
118 }
119 }
120 };
121}
122
123newtype_id!(
124 MarketSymbol
126);
127
128impl IntoValidId<MarketSymbol> for &str {
129 fn into_valid(self) -> Result<MarketSymbol, O2Error> {
130 MarketSymbol::parse(self)
131 }
132}
133
134impl IntoValidId<MarketSymbol> for String {
135 fn into_valid(self) -> Result<MarketSymbol, O2Error> {
136 MarketSymbol::parse(self)
137 }
138}
139
140impl IntoValidId<MarketSymbol> for MarketSymbol {
141 fn into_valid(self) -> Result<MarketSymbol, O2Error> {
142 Ok(self)
143 }
144}
145
146impl IntoValidId<MarketSymbol> for &MarketSymbol {
147 fn into_valid(self) -> Result<MarketSymbol, O2Error> {
148 Ok(self.clone())
149 }
150}
151
152impl MarketSymbol {
153 pub fn parse(input: impl AsRef<str>) -> Result<Self, O2Error> {
157 Self::from_str(input.as_ref())
158 }
159}
160
161impl FromStr for MarketSymbol {
162 type Err = O2Error;
163
164 fn from_str(s: &str) -> Result<Self, Self::Err> {
165 let trimmed = s.trim();
166 if trimmed.is_empty() {
167 return Err(O2Error::InvalidRequest(
168 "Market symbol cannot be empty".to_string(),
169 ));
170 }
171
172 let (base_raw, quote_raw) = trimmed.split_once('/').ok_or_else(|| {
173 O2Error::InvalidRequest(format!(
174 "Invalid market symbol '{trimmed}'. Expected format BASE/QUOTE"
175 ))
176 })?;
177
178 if quote_raw.contains('/') {
179 return Err(O2Error::InvalidRequest(format!(
180 "Invalid market symbol '{trimmed}'. Expected exactly one '/' separator"
181 )));
182 }
183
184 let base = base_raw.trim();
185 let quote = quote_raw.trim();
186 if base.is_empty() || quote.is_empty() {
187 return Err(O2Error::InvalidRequest(format!(
188 "Invalid market symbol '{trimmed}'. Base and quote must be non-empty"
189 )));
190 }
191
192 Ok(MarketSymbol::new(format!("{base}/{quote}")))
193 }
194}
195
196pub trait IntoMarketSymbol {
198 fn into_market_symbol(self) -> Result<MarketSymbol, O2Error>;
199}
200
201impl IntoMarketSymbol for MarketSymbol {
202 fn into_market_symbol(self) -> Result<MarketSymbol, O2Error> {
203 MarketSymbol::parse(self.as_str())
204 }
205}
206
207impl IntoMarketSymbol for &MarketSymbol {
208 fn into_market_symbol(self) -> Result<MarketSymbol, O2Error> {
209 MarketSymbol::parse(self.as_str())
210 }
211}
212
213impl IntoMarketSymbol for &str {
214 fn into_market_symbol(self) -> Result<MarketSymbol, O2Error> {
215 MarketSymbol::parse(self)
216 }
217}
218
219impl IntoMarketSymbol for String {
220 fn into_market_symbol(self) -> Result<MarketSymbol, O2Error> {
221 MarketSymbol::parse(self)
222 }
223}
224
225impl IntoMarketSymbol for &String {
226 fn into_market_symbol(self) -> Result<MarketSymbol, O2Error> {
227 MarketSymbol::parse(self)
228 }
229}
230
231hex_id!(
232 ContractId
234);
235hex_id!(
236 MarketId
238);
239hex_id!(
240 OrderId
242);
243hex_id!(
244 TradeId
246);
247hex_id!(
248 TradeAccountId
250);
251hex_id!(
252 AssetId
254);
255
256fn normalize_hex_prefixed(s: String) -> String {
257 if s.starts_with("0x") || s.starts_with("0X") || s.is_empty() {
258 s
259 } else {
260 format!("0x{s}")
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Default)]
266#[serde(transparent)]
267pub struct TxId(String);
268
269impl TxId {
270 pub fn new(s: impl Into<String>) -> Self {
271 Self(normalize_hex_prefixed(s.into()))
272 }
273 pub fn as_str(&self) -> &str {
274 &self.0
275 }
276}
277
278impl AsRef<str> for TxId {
279 fn as_ref(&self) -> &str {
280 &self.0
281 }
282}
283
284impl std::fmt::Display for TxId {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 f.write_str(&self.0)
287 }
288}
289
290impl From<String> for TxId {
291 fn from(s: String) -> Self {
292 Self::new(s)
293 }
294}
295
296impl From<&str> for TxId {
297 fn from(s: &str) -> Self {
298 Self::new(s)
299 }
300}
301
302impl std::ops::Deref for TxId {
303 type Target = str;
304 fn deref(&self) -> &str {
305 &self.0
306 }
307}
308
309impl<'de> Deserialize<'de> for TxId {
310 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
311 where
312 D: Deserializer<'de>,
313 {
314 let raw = String::deserialize(deserializer)?;
315 Ok(TxId::new(raw))
316 }
317}
318
319fn deserialize_string_or_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
321where
322 D: Deserializer<'de>,
323{
324 use serde::de;
325
326 struct StringOrU64;
327 impl<'de> de::Visitor<'de> for StringOrU64 {
328 type Value = u64;
329 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
330 f.write_str("a u64 or a string containing a decimal/0x-hex u64")
331 }
332 fn visit_u64<E: de::Error>(self, v: u64) -> Result<u64, E> {
333 Ok(v)
334 }
335 fn visit_i64<E: de::Error>(self, v: i64) -> Result<u64, E> {
336 u64::try_from(v).map_err(de::Error::custom)
337 }
338 fn visit_str<E: de::Error>(self, v: &str) -> Result<u64, E> {
339 if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
340 u64::from_str_radix(hex, 16).map_err(de::Error::custom)
341 } else {
342 v.parse().map_err(de::Error::custom)
343 }
344 }
345 }
346 deserializer.deserialize_any(StringOrU64)
347}
348
349fn deserialize_optional_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
351where
352 D: Deserializer<'de>,
353{
354 let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
355 match value {
356 Some(serde_json::Value::String(s)) => {
357 if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
358 u64::from_str_radix(hex, 16)
359 .map(Some)
360 .map_err(serde::de::Error::custom)
361 } else {
362 s.parse().map(Some).map_err(serde::de::Error::custom)
363 }
364 }
365 Some(serde_json::Value::Number(n)) => n
366 .as_u64()
367 .ok_or_else(|| serde::de::Error::custom("number is not u64"))
368 .map(Some),
369 Some(serde_json::Value::Null) | None => Ok(None),
370 Some(v) => Err(serde::de::Error::custom(format!(
371 "expected string/number/null for u64 field, got {v}"
372 ))),
373 }
374}
375
376fn deserialize_string_or_u128<'de, D>(deserializer: D) -> Result<u128, D::Error>
378where
379 D: Deserializer<'de>,
380{
381 use serde::de;
382
383 struct StringOrU128;
384 impl<'de> de::Visitor<'de> for StringOrU128 {
385 type Value = u128;
386 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
387 f.write_str("a u128 or a string containing a decimal u128")
388 }
389 fn visit_u64<E: de::Error>(self, v: u64) -> Result<u128, E> {
390 Ok(v as u128)
391 }
392 fn visit_u128<E: de::Error>(self, v: u128) -> Result<u128, E> {
393 Ok(v)
394 }
395 fn visit_i64<E: de::Error>(self, v: i64) -> Result<u128, E> {
396 u128::try_from(v).map_err(de::Error::custom)
397 }
398 fn visit_str<E: de::Error>(self, v: &str) -> Result<u128, E> {
399 v.parse().map_err(de::Error::custom)
400 }
401 }
402 deserializer.deserialize_any(StringOrU128)
403}
404
405fn deserialize_string_or_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
407where
408 D: Deserializer<'de>,
409{
410 use serde::de;
411
412 struct StringOrF64;
413 impl<'de> de::Visitor<'de> for StringOrF64 {
414 type Value = f64;
415 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
416 f.write_str("an f64 or a string containing an f64")
417 }
418 fn visit_f64<E: de::Error>(self, v: f64) -> Result<f64, E> {
419 Ok(v)
420 }
421 fn visit_u64<E: de::Error>(self, v: u64) -> Result<f64, E> {
422 Ok(v as f64)
423 }
424 fn visit_i64<E: de::Error>(self, v: i64) -> Result<f64, E> {
425 Ok(v as f64)
426 }
427 fn visit_str<E: de::Error>(self, v: &str) -> Result<f64, E> {
428 v.parse().map_err(de::Error::custom)
429 }
430 }
431 deserializer.deserialize_any(StringOrF64)
432}
433
434fn deserialize_optional_f64<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
436where
437 D: Deserializer<'de>,
438{
439 let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
440 match value {
441 Some(serde_json::Value::String(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
442 Some(serde_json::Value::Number(n)) => n
443 .as_f64()
444 .ok_or_else(|| serde::de::Error::custom("number is not f64"))
445 .map(Some),
446 Some(serde_json::Value::Null) | None => Ok(None),
447 Some(v) => Err(serde::de::Error::custom(format!(
448 "expected string/number/null for f64 field, got {v}"
449 ))),
450 }
451}
452
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
459pub enum Side {
460 Buy,
461 Sell,
462}
463
464impl Side {
465 pub fn as_str(&self) -> &str {
467 match self {
468 Side::Buy => "Buy",
469 Side::Sell => "Sell",
470 }
471 }
472}
473
474impl std::fmt::Display for Side {
475 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476 f.write_str(self.as_str())
477 }
478}
479
480impl Serialize for Side {
481 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
482 serializer.serialize_str(self.as_str())
483 }
484}
485
486impl<'de> Deserialize<'de> for Side {
487 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
488 let raw = String::deserialize(deserializer)?;
489 match raw.to_ascii_lowercase().as_str() {
490 "buy" => Ok(Side::Buy),
491 "sell" => Ok(Side::Sell),
492 _ => Err(serde::de::Error::custom(format!("invalid side '{raw}'"))),
493 }
494 }
495}
496
497#[derive(Debug, Clone)]
503pub enum OrderType {
504 Spot,
505 Market,
506 FillOrKill,
507 PostOnly,
508 Limit {
509 price: UnsignedDecimal,
510 timestamp: u64,
511 },
512 BoundedMarket {
513 max_price: UnsignedDecimal,
514 min_price: UnsignedDecimal,
515 },
516}
517
518#[derive(Debug, Clone)]
522pub enum Action {
523 CreateOrder {
524 side: Side,
525 price: UnsignedDecimal,
526 quantity: UnsignedDecimal,
527 order_type: OrderType,
528 },
529 CancelOrder {
530 order_id: OrderId,
531 },
532 SettleBalance,
533 RegisterReferer {
534 to: Identity,
535 },
536}
537
538#[derive(Debug, Clone, PartialEq, Eq)]
540pub struct Price {
541 value: UnsignedDecimal,
542 market_id: MarketId,
543 quote_decimals: u32,
544 quote_max_precision: u32,
545}
546
547impl Price {
548 pub fn value(&self) -> UnsignedDecimal {
550 self.value
551 }
552
553 pub fn market_id(&self) -> &MarketId {
555 &self.market_id
556 }
557}
558
559impl std::fmt::Display for Price {
560 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
561 self.value.fmt(f)
562 }
563}
564
565#[derive(Debug, Clone, PartialEq, Eq)]
567pub struct Quantity {
568 value: UnsignedDecimal,
569 market_id: MarketId,
570 base_decimals: u32,
571 base_max_precision: u32,
572}
573
574impl Quantity {
575 pub fn value(&self) -> UnsignedDecimal {
577 self.value
578 }
579
580 pub fn market_id(&self) -> &MarketId {
582 &self.market_id
583 }
584}
585
586impl std::fmt::Display for Quantity {
587 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
588 self.value.fmt(f)
589 }
590}
591
592#[derive(Debug, Clone, PartialEq, Eq)]
594pub enum OrderPriceInput {
595 Unchecked(UnsignedDecimal),
597 Checked(Price),
599}
600
601impl TryFrom<UnsignedDecimal> for OrderPriceInput {
602 type Error = O2Error;
603 fn try_from(value: UnsignedDecimal) -> Result<Self, Self::Error> {
604 Ok(Self::Unchecked(value))
605 }
606}
607
608impl TryFrom<Price> for OrderPriceInput {
609 type Error = O2Error;
610 fn try_from(value: Price) -> Result<Self, Self::Error> {
611 Ok(Self::Checked(value))
612 }
613}
614
615impl TryFrom<&str> for OrderPriceInput {
616 type Error = O2Error;
617 fn try_from(value: &str) -> Result<Self, Self::Error> {
618 Ok(Self::Unchecked(value.parse()?))
619 }
620}
621
622impl TryFrom<String> for OrderPriceInput {
623 type Error = O2Error;
624 fn try_from(value: String) -> Result<Self, Self::Error> {
625 Ok(Self::Unchecked(value.parse()?))
626 }
627}
628
629#[derive(Debug, Clone, PartialEq, Eq)]
631pub enum OrderQuantityInput {
632 Unchecked(UnsignedDecimal),
634 Checked(Quantity),
636}
637
638impl TryFrom<UnsignedDecimal> for OrderQuantityInput {
639 type Error = O2Error;
640 fn try_from(value: UnsignedDecimal) -> Result<Self, Self::Error> {
641 Ok(Self::Unchecked(value))
642 }
643}
644
645impl TryFrom<Quantity> for OrderQuantityInput {
646 type Error = O2Error;
647 fn try_from(value: Quantity) -> Result<Self, Self::Error> {
648 Ok(Self::Checked(value))
649 }
650}
651
652impl TryFrom<&str> for OrderQuantityInput {
653 type Error = O2Error;
654 fn try_from(value: &str) -> Result<Self, Self::Error> {
655 Ok(Self::Unchecked(value.parse()?))
656 }
657}
658
659impl TryFrom<String> for OrderQuantityInput {
660 type Error = O2Error;
661 fn try_from(value: String) -> Result<Self, Self::Error> {
662 Ok(Self::Unchecked(value.parse()?))
663 }
664}
665
666impl OrderType {
667 pub fn to_encoding(
670 &self,
671 market: &Market,
672 ) -> Result<(crate::encoding::OrderTypeEncoding, serde_json::Value), O2Error> {
673 use crate::encoding::OrderTypeEncoding;
674 match self {
675 OrderType::Spot => Ok((OrderTypeEncoding::Spot, serde_json::json!("Spot"))),
676 OrderType::Market => Ok((OrderTypeEncoding::Market, serde_json::json!("Market"))),
677 OrderType::FillOrKill => Ok((
678 OrderTypeEncoding::FillOrKill,
679 serde_json::json!("FillOrKill"),
680 )),
681 OrderType::PostOnly => Ok((OrderTypeEncoding::PostOnly, serde_json::json!("PostOnly"))),
682 OrderType::Limit { price, timestamp } => {
683 let scaled_price = market.scale_price(price)?;
684 Ok((
685 OrderTypeEncoding::Limit {
686 price: scaled_price,
687 timestamp: *timestamp,
688 },
689 serde_json::json!({ "Limit": [scaled_price.to_string(), timestamp.to_string()] }),
690 ))
691 }
692 OrderType::BoundedMarket {
693 max_price,
694 min_price,
695 } => {
696 let scaled_max = market.scale_price(max_price)?;
697 let scaled_min = market.scale_price(min_price)?;
698 Ok((
699 OrderTypeEncoding::BoundedMarket {
700 max_price: scaled_max,
701 min_price: scaled_min,
702 },
703 serde_json::json!({ "BoundedMarket": { "max_price": scaled_max.to_string(), "min_price": scaled_min.to_string() } }),
704 ))
705 }
706 }
707 }
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
716pub enum Identity {
717 Address(String),
718 ContractId(String),
719}
720
721impl Identity {
722 pub fn address_value(&self) -> &str {
723 match self {
724 Identity::Address(a) => a,
725 Identity::ContractId(c) => c,
726 }
727 }
728}
729
730#[derive(Debug, Clone, Serialize, Deserialize)]
732pub enum Signature {
733 Secp256k1(String),
734}
735
736#[derive(Debug, Clone, Serialize, Deserialize)]
742pub struct MarketAsset {
743 pub symbol: String,
744 pub asset: AssetId,
745 pub decimals: u32,
746 pub max_precision: u32,
747}
748
749#[derive(Debug, Clone, Serialize, Deserialize)]
751pub struct Market {
752 pub contract_id: ContractId,
753 pub market_id: MarketId,
754 pub whitelist_id: Option<ContractId>,
755 pub blacklist_id: Option<ContractId>,
756 #[serde(deserialize_with = "deserialize_string_or_u64")]
757 pub maker_fee: u64,
758 #[serde(deserialize_with = "deserialize_string_or_u64")]
759 pub taker_fee: u64,
760 #[serde(deserialize_with = "deserialize_string_or_u64")]
761 pub min_order: u64,
762 #[serde(deserialize_with = "deserialize_string_or_u64")]
763 pub dust: u64,
764 #[serde(deserialize_with = "deserialize_string_or_u64")]
765 pub price_window: u64,
766 pub base: MarketAsset,
767 pub quote: MarketAsset,
768}
769
770impl Market {
771 fn parsed_unsigned(value: &str, field: &str) -> Result<UnsignedDecimal, O2Error> {
772 UnsignedDecimal::from_str(value)
773 .map_err(|e| O2Error::InvalidOrderParams(format!("Invalid {field}: {e}")))
774 }
775
776 fn decimal_scale(value: &UnsignedDecimal) -> u32 {
777 value.inner().normalize().scale()
778 }
779
780 pub fn price(&self, value: &str) -> Result<Price, O2Error> {
782 let parsed = Self::parsed_unsigned(value, "price")?;
783 self.price_from_decimal(parsed)
784 }
785
786 pub fn price_from_decimal(&self, value: UnsignedDecimal) -> Result<Price, O2Error> {
788 let scale = Self::decimal_scale(&value);
789 if scale > self.quote.max_precision {
790 return Err(O2Error::InvalidOrderParams(format!(
791 "Price precision {} exceeds max {} for market {}",
792 scale, self.quote.max_precision, self.market_id
793 )));
794 }
795 let _ = self.scale_price(&value)?;
797 Ok(Price {
798 value,
799 market_id: self.market_id.clone(),
800 quote_decimals: self.quote.decimals,
801 quote_max_precision: self.quote.max_precision,
802 })
803 }
804
805 pub fn quantity(&self, value: &str) -> Result<Quantity, O2Error> {
807 let parsed = Self::parsed_unsigned(value, "quantity")?;
808 self.quantity_from_decimal(parsed)
809 }
810
811 pub fn quantity_from_decimal(&self, value: UnsignedDecimal) -> Result<Quantity, O2Error> {
813 let scale = Self::decimal_scale(&value);
814 if scale > self.base.max_precision {
815 return Err(O2Error::InvalidOrderParams(format!(
816 "Quantity precision {} exceeds max {} for market {}",
817 scale, self.base.max_precision, self.market_id
818 )));
819 }
820 let _ = self.scale_quantity(&value)?;
822 Ok(Quantity {
823 value,
824 market_id: self.market_id.clone(),
825 base_decimals: self.base.decimals,
826 base_max_precision: self.base.max_precision,
827 })
828 }
829
830 pub fn validate_price_binding(&self, price: &Price) -> Result<(), O2Error> {
832 if price.market_id != self.market_id
833 || price.quote_decimals != self.quote.decimals
834 || price.quote_max_precision != self.quote.max_precision
835 {
836 return Err(O2Error::Other(format!(
837 "Price wrapper is stale or bound to a different market (expected {}, got {})",
838 self.market_id, price.market_id
839 )));
840 }
841 Ok(())
842 }
843
844 pub fn validate_quantity_binding(&self, quantity: &Quantity) -> Result<(), O2Error> {
846 if quantity.market_id != self.market_id
847 || quantity.base_decimals != self.base.decimals
848 || quantity.base_max_precision != self.base.max_precision
849 {
850 return Err(O2Error::Other(format!(
851 "Quantity wrapper is stale or bound to a different market (expected {}, got {})",
852 self.market_id, quantity.market_id
853 )));
854 }
855 Ok(())
856 }
857
858 fn checked_pow_u64(exp: u32, field: &str) -> Result<u64, O2Error> {
859 10u64
860 .checked_pow(exp)
861 .ok_or_else(|| O2Error::Other(format!("Invalid {field}: 10^{exp} overflows u64")))
862 }
863
864 fn checked_pow_u128(exp: u32, field: &str) -> Result<u128, O2Error> {
865 10u128
866 .checked_pow(exp)
867 .ok_or_else(|| O2Error::Other(format!("Invalid {field}: 10^{exp} overflows u128")))
868 }
869
870 fn checked_truncate_factor(
871 decimals: u32,
872 max_precision: u32,
873 field: &str,
874 ) -> Result<u64, O2Error> {
875 if max_precision > decimals {
876 return Err(O2Error::Other(format!(
877 "Invalid {field}: max_precision ({max_precision}) exceeds decimals ({decimals})"
878 )));
879 }
880 Self::checked_pow_u64(decimals - max_precision, field)
881 }
882
883 pub fn format_price(&self, chain_value: u64) -> UnsignedDecimal {
885 let factor = 10u64.pow(self.quote.decimals);
886 let d = Decimal::from(chain_value) / Decimal::from(factor);
887 UnsignedDecimal::new(d).unwrap()
888 }
889
890 pub fn scale_price(&self, human_value: &UnsignedDecimal) -> Result<u64, O2Error> {
892 let factor_u64 = Self::checked_pow_u64(self.quote.decimals, "quote.decimals")?;
893 let factor = Decimal::from(factor_u64);
894 let scaled_str = (*human_value.inner() * factor).floor().to_string();
895 let scaled = scaled_str.parse::<u64>().map_err(|e| {
896 O2Error::Other(format!(
897 "Failed to scale price '{}' into u64: {e}",
898 human_value
899 ))
900 })?;
901 let truncate_factor = Self::checked_truncate_factor(
902 self.quote.decimals,
903 self.quote.max_precision,
904 "quote precision",
905 )?;
906 Ok((scaled / truncate_factor) * truncate_factor)
907 }
908
909 pub fn format_quantity(&self, chain_value: u64) -> UnsignedDecimal {
911 let factor = 10u64.pow(self.base.decimals);
912 let d = Decimal::from(chain_value) / Decimal::from(factor);
913 UnsignedDecimal::new(d).unwrap()
914 }
915
916 pub fn scale_quantity(&self, human_value: &UnsignedDecimal) -> Result<u64, O2Error> {
918 let factor_u64 = Self::checked_pow_u64(self.base.decimals, "base.decimals")?;
919 let factor = Decimal::from(factor_u64);
920 let scaled_str = (*human_value.inner() * factor).floor().to_string();
921 let scaled = scaled_str.parse::<u64>().map_err(|e| {
922 O2Error::Other(format!(
923 "Failed to scale quantity '{}' into u64: {e}",
924 human_value
925 ))
926 })?;
927 let truncate_factor = Self::checked_truncate_factor(
928 self.base.decimals,
929 self.base.max_precision,
930 "base precision",
931 )?;
932 Ok((scaled / truncate_factor) * truncate_factor)
933 }
934
935 pub fn symbol_pair(&self) -> MarketSymbol {
937 MarketSymbol::new(format!("{}/{}", self.base.symbol, self.quote.symbol))
938 }
939
940 pub fn adjust_quantity(&self, price: u64, quantity: u64) -> Result<u64, O2Error> {
943 if price == 0 {
944 return Err(O2Error::InvalidOrderParams(
945 "Price cannot be zero when adjusting quantity".into(),
946 ));
947 }
948 let base_factor = Self::checked_pow_u128(self.base.decimals, "base.decimals")?;
949 let product = price as u128 * quantity as u128;
950 let remainder = product % base_factor;
951 if remainder == 0 {
952 return Ok(quantity);
953 }
954 let adjusted_product = product - remainder;
955 let adjusted = adjusted_product / price as u128;
956 if adjusted > u64::MAX as u128 {
957 return Err(O2Error::InvalidOrderParams(
958 "Adjusted quantity exceeds u64 range".into(),
959 ));
960 }
961 Ok(adjusted as u64)
962 }
963
964 pub fn validate_order(&self, price: u64, quantity: u64) -> Result<(), O2Error> {
966 let base_factor = Self::checked_pow_u128(self.base.decimals, "base.decimals")?;
967 let quote_value = (price as u128 * quantity as u128) / base_factor;
968 let min_order: u128 = self.min_order as u128;
969 if quote_value < min_order {
970 return Err(O2Error::InvalidOrderParams(format!(
971 "Quote value {} below min_order {}",
972 quote_value, min_order
973 )));
974 }
975 if (price as u128 * quantity as u128) % base_factor != 0 {
977 return Err(O2Error::InvalidOrderParams(
978 "FractionalPrice: (price * quantity) % 10^base_decimals != 0".into(),
979 ));
980 }
981 Ok(())
982 }
983}
984
985#[derive(Debug, Clone, Serialize, Deserialize)]
987pub struct MarketsResponse {
988 pub books_registry_id: ContractId,
989 pub books_whitelist_id: Option<ContractId>,
990 pub books_blacklist_id: Option<ContractId>,
991 pub accounts_registry_id: ContractId,
992 pub trade_account_oracle_id: ContractId,
993 pub fast_bridge_asset_registry_contract_id: Option<ContractId>,
994 pub chain_id: String,
995 pub base_asset_id: AssetId,
996 pub markets: Vec<Market>,
997}
998
999#[derive(Debug, Clone, Serialize, Deserialize)]
1001pub struct MarketSummary {
1002 pub market_id: MarketId,
1003 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1004 pub high_price: Option<u64>,
1005 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1006 pub low_price: Option<u64>,
1007 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1008 pub last_price: Option<u64>,
1009 #[serde(deserialize_with = "deserialize_string_or_u128")]
1010 pub volume_24h: u128,
1011 #[serde(deserialize_with = "deserialize_string_or_f64")]
1012 pub change_24h: f64,
1013}
1014
1015#[derive(Debug, Clone, Serialize, Deserialize)]
1017pub struct MarketTicker {
1018 pub market_id: MarketId,
1019 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1020 pub high: Option<u64>,
1021 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1022 pub low: Option<u64>,
1023 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1024 pub bid: Option<u64>,
1025 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1026 pub bid_volume: Option<u64>,
1027 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1028 pub ask: Option<u64>,
1029 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1030 pub ask_volume: Option<u64>,
1031 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1032 pub open: Option<u64>,
1033 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1034 pub close: Option<u64>,
1035 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1036 pub last: Option<u64>,
1037 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1038 pub previous_close: Option<u64>,
1039 #[serde(default, deserialize_with = "deserialize_optional_f64")]
1040 pub change: Option<f64>,
1041 #[serde(default, deserialize_with = "deserialize_optional_f64")]
1042 pub percentage: Option<f64>,
1043 #[serde(default, deserialize_with = "deserialize_optional_f64")]
1044 pub average: Option<f64>,
1045 #[serde(deserialize_with = "deserialize_string_or_u128")]
1046 pub base_volume: u128,
1047 #[serde(deserialize_with = "deserialize_string_or_u128")]
1048 pub quote_volume: u128,
1049 #[serde(deserialize_with = "deserialize_string_or_u128")]
1050 pub timestamp: u128,
1051}
1052
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1059pub struct TradeAccount {
1060 #[serde(default)]
1061 pub last_modification: u64,
1062 #[serde(default, deserialize_with = "deserialize_string_or_u64")]
1063 pub nonce: u64,
1064 pub owner: Identity,
1065 #[serde(default)]
1066 pub synced_with_network: Option<bool>,
1067 #[serde(default)]
1068 pub sync_state: Option<serde_json::Value>,
1069}
1070
1071#[derive(Debug, Clone, Serialize, Deserialize)]
1073pub struct AccountResponse {
1074 pub trade_account_id: Option<TradeAccountId>,
1075 pub trade_account: Option<TradeAccount>,
1076 pub session: Option<SessionInfo>,
1077}
1078
1079#[derive(Debug, Clone, Serialize, Deserialize)]
1081pub struct SessionInfo {
1082 pub session_id: Identity,
1083 #[serde(deserialize_with = "deserialize_string_or_u64")]
1084 pub expiry: u64,
1085 pub contract_ids: Vec<ContractId>,
1086}
1087
1088#[derive(Debug, Clone, Serialize, Deserialize)]
1090pub struct CreateAccountResponse {
1091 pub trade_account_id: TradeAccountId,
1092 #[serde(deserialize_with = "deserialize_string_or_u64")]
1093 pub nonce: u64,
1094}
1095
1096#[derive(Debug, Clone, Serialize, Deserialize)]
1102pub struct SessionRequest {
1103 pub contract_id: TradeAccountId,
1104 pub session_id: Identity,
1105 pub signature: Signature,
1106 pub contract_ids: Vec<ContractId>,
1107 pub nonce: String,
1108 pub expiry: String,
1109}
1110
1111#[derive(Debug, Clone, Serialize, Deserialize)]
1113pub struct SessionResponse {
1114 pub tx_id: TxId,
1115 pub trade_account_id: TradeAccountId,
1116 pub contract_ids: Vec<ContractId>,
1117 pub session_id: Identity,
1118 #[serde(deserialize_with = "deserialize_string_or_u64")]
1119 pub session_expiry: u64,
1120}
1121
1122#[derive(Debug, Clone)]
1124pub struct Session {
1125 pub owner_address: [u8; 32],
1126 pub session_private_key: [u8; 32],
1127 pub session_address: [u8; 32],
1128 pub trade_account_id: TradeAccountId,
1129 pub contract_ids: Vec<ContractId>,
1130 pub expiry: u64,
1131 pub nonce: u64,
1132}
1133
1134#[derive(Debug, Clone, Serialize, Deserialize)]
1140pub struct Order {
1141 #[serde(default)]
1142 pub order_id: OrderId,
1143 pub side: Side,
1144 pub order_type: serde_json::Value,
1145 #[serde(default, deserialize_with = "deserialize_string_or_u64")]
1146 pub quantity: u64,
1147 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1148 pub quantity_fill: Option<u64>,
1149 #[serde(default, deserialize_with = "deserialize_string_or_u64")]
1150 pub price: u64,
1151 #[serde(default, deserialize_with = "deserialize_optional_u64")]
1152 pub price_fill: Option<u64>,
1153 pub timestamp: Option<serde_json::Value>,
1154 #[serde(default)]
1155 pub close: bool,
1156 #[serde(default)]
1157 pub partially_filled: bool,
1158 #[serde(default)]
1159 pub cancel: bool,
1160 #[serde(default)]
1161 pub desired_quantity: Option<serde_json::Value>,
1162 #[serde(default)]
1163 pub base_decimals: Option<u32>,
1164 #[serde(default)]
1165 pub account: Option<Identity>,
1166 #[serde(default)]
1167 pub fill: Option<serde_json::Value>,
1168 #[serde(default)]
1169 pub order_tx_history: Option<Vec<serde_json::Value>>,
1170 #[serde(default)]
1171 pub market_id: Option<MarketId>,
1172 #[serde(default)]
1173 pub owner: Option<Identity>,
1174 #[serde(default)]
1175 pub history: Option<Vec<serde_json::Value>>,
1176 #[serde(default)]
1177 pub fills: Option<Vec<serde_json::Value>>,
1178}
1179
1180#[derive(Debug, Clone, Serialize, Deserialize)]
1182pub struct OrdersResponse {
1183 pub identity: Identity,
1184 pub market_id: MarketId,
1185 #[serde(default)]
1186 pub orders: Vec<Order>,
1187}
1188
1189#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1198#[serde(rename_all = "lowercase")]
1199pub enum TraderSide {
1200 Maker,
1201 Taker,
1202 Both,
1203}
1204
1205#[derive(Debug, Clone, Serialize, Deserialize)]
1211pub struct Trade {
1212 pub trade_id: TradeId,
1213 pub side: Side,
1215 #[serde(deserialize_with = "deserialize_string_or_u128")]
1216 pub total: u128,
1217 #[serde(deserialize_with = "deserialize_string_or_u64")]
1218 pub quantity: u64,
1219 #[serde(deserialize_with = "deserialize_string_or_u64")]
1220 pub price: u64,
1221 #[serde(deserialize_with = "deserialize_string_or_u128")]
1223 pub timestamp: u128,
1224 #[serde(default)]
1226 pub trader_side: Option<TraderSide>,
1227 #[serde(default)]
1228 pub maker: Option<Identity>,
1229 #[serde(default)]
1230 pub taker: Option<Identity>,
1231}
1232
1233#[derive(Debug, Clone, Serialize, Deserialize)]
1235pub struct TradesResponse {
1236 #[serde(default)]
1237 pub trades: Vec<Trade>,
1238 pub market_id: MarketId,
1239}
1240
1241#[derive(Debug, Clone, Serialize, Deserialize)]
1247pub struct OrderBookBalance {
1248 #[serde(deserialize_with = "deserialize_string_or_u128")]
1249 pub locked: u128,
1250 #[serde(deserialize_with = "deserialize_string_or_u128")]
1251 pub unlocked: u128,
1252 #[serde(deserialize_with = "deserialize_string_or_u128")]
1253 pub fee: u128,
1254}
1255
1256#[derive(Debug, Clone, Serialize, Deserialize)]
1280pub struct BalanceResponse {
1281 pub order_books: HashMap<String, OrderBookBalance>,
1282 #[serde(deserialize_with = "deserialize_string_or_u128")]
1284 pub total_locked: u128,
1285 #[serde(deserialize_with = "deserialize_string_or_u128")]
1291 pub total_unlocked: u128,
1292 #[serde(deserialize_with = "deserialize_string_or_u128")]
1297 pub trading_account_balance: u128,
1298}
1299
1300impl BalanceResponse {
1301 #[inline]
1305 pub fn available(&self) -> u128 {
1306 self.total_unlocked
1307 }
1308
1309 #[inline]
1311 pub fn locked(&self) -> u128 {
1312 self.total_locked
1313 }
1314
1315 #[inline]
1317 pub fn total(&self) -> u128 {
1318 self.total_unlocked + self.total_locked
1319 }
1320}
1321
1322#[derive(Debug, Clone, Serialize, Deserialize)]
1328pub struct DepthLevel {
1329 #[serde(deserialize_with = "deserialize_string_or_u64")]
1330 pub price: u64,
1331 #[serde(deserialize_with = "deserialize_string_or_u64")]
1332 pub quantity: u64,
1333}
1334
1335#[derive(Debug, Clone, Serialize, Deserialize)]
1337pub struct DepthSnapshot {
1338 #[serde(default, rename = "buys")]
1340 pub bids: Vec<DepthLevel>,
1341 #[serde(default, rename = "sells")]
1343 pub asks: Vec<DepthLevel>,
1344}
1345
1346#[derive(Debug, Clone, Serialize, Deserialize)]
1348pub struct DepthUpdate {
1349 pub action: String,
1350 pub changes: Option<DepthSnapshot>,
1351 #[serde(alias = "view")]
1352 pub view: Option<DepthSnapshot>,
1353 pub market_id: MarketId,
1354 pub onchain_timestamp: Option<String>,
1355 pub seen_timestamp: Option<String>,
1356}
1357
1358#[derive(Debug, Clone, Serialize, Deserialize)]
1364pub struct Bar {
1365 #[serde(deserialize_with = "deserialize_string_or_u64")]
1366 pub open: u64,
1367 #[serde(deserialize_with = "deserialize_string_or_u64")]
1368 pub high: u64,
1369 #[serde(deserialize_with = "deserialize_string_or_u64")]
1370 pub low: u64,
1371 #[serde(deserialize_with = "deserialize_string_or_u64")]
1372 pub close: u64,
1373 #[serde(deserialize_with = "deserialize_string_or_u128")]
1374 pub buy_volume: u128,
1375 #[serde(deserialize_with = "deserialize_string_or_u128")]
1376 pub sell_volume: u128,
1377 #[serde(deserialize_with = "deserialize_string_or_u128")]
1378 pub timestamp: u128,
1379}
1380
1381#[derive(Debug, Clone, Serialize, Deserialize)]
1387pub struct CreateOrderAction {
1388 pub side: String,
1389 pub price: String,
1390 pub quantity: String,
1391 pub order_type: serde_json::Value,
1392}
1393
1394#[derive(Debug, Clone, Serialize, Deserialize)]
1396pub struct CancelOrderAction {
1397 pub order_id: String,
1398}
1399
1400#[derive(Debug, Clone, Serialize, Deserialize)]
1402pub struct SettleBalanceAction {
1403 pub to: Identity,
1404}
1405
1406#[derive(Debug, Clone, Serialize, Deserialize)]
1408#[serde(untagged)]
1409pub enum ActionItem {
1410 CreateOrder {
1411 #[serde(rename = "CreateOrder")]
1412 create_order: CreateOrderAction,
1413 },
1414 CancelOrder {
1415 #[serde(rename = "CancelOrder")]
1416 cancel_order: CancelOrderAction,
1417 },
1418 SettleBalance {
1419 #[serde(rename = "SettleBalance")]
1420 settle_balance: SettleBalanceAction,
1421 },
1422 RegisterReferer {
1423 #[serde(rename = "RegisterReferer")]
1424 register_referer: SettleBalanceAction,
1425 },
1426}
1427
1428#[derive(Debug, Clone, Serialize, Deserialize)]
1430pub(crate) struct MarketActions {
1431 pub market_id: MarketId,
1432 pub actions: Vec<serde_json::Value>,
1433}
1434
1435#[derive(Debug, Clone, Serialize, Deserialize)]
1437pub(crate) struct SessionActionsRequest {
1438 pub actions: Vec<MarketActions>,
1439 pub signature: Signature,
1440 pub nonce: String,
1441 pub trade_account_id: TradeAccountId,
1442 pub session_id: Identity,
1443 #[serde(skip_serializing_if = "Option::is_none")]
1444 pub collect_orders: Option<bool>,
1445 #[serde(skip_serializing_if = "Option::is_none")]
1446 pub variable_outputs: Option<u32>,
1447}
1448
1449#[derive(Debug, Clone, Serialize, Deserialize)]
1451pub struct SessionActionsResponse {
1452 pub tx_id: Option<TxId>,
1453 pub orders: Option<Vec<Order>>,
1454 pub code: Option<u32>,
1456 pub message: Option<String>,
1457 pub reason: Option<String>,
1458 pub receipts: Option<serde_json::Value>,
1459}
1460
1461impl SessionActionsResponse {
1462 pub fn is_success(&self) -> bool {
1464 self.tx_id.is_some()
1465 }
1466
1467 pub fn is_preflight_error(&self) -> bool {
1469 self.code.is_some() && self.tx_id.is_none()
1470 }
1471
1472 pub fn is_onchain_error(&self) -> bool {
1474 self.message.is_some() && self.code.is_none() && self.tx_id.is_none()
1475 }
1476}
1477
1478#[derive(Debug, Clone, Serialize, Deserialize)]
1484pub struct WithdrawRequest {
1485 pub trade_account_id: TradeAccountId,
1486 pub signature: Signature,
1487 pub nonce: String,
1488 pub to: Identity,
1489 pub asset_id: AssetId,
1490 pub amount: String,
1491}
1492
1493#[derive(Debug, Clone, Serialize, Deserialize)]
1495pub struct WithdrawResponse {
1496 pub tx_id: Option<String>,
1497 pub code: Option<u32>,
1498 pub message: Option<String>,
1499}
1500
1501#[derive(Debug, Clone, Serialize, Deserialize)]
1507pub struct WhitelistRequest {
1508 #[serde(rename = "tradeAccount")]
1509 pub trade_account: String,
1510}
1511
1512#[derive(Debug, Clone, Serialize, Deserialize)]
1514pub struct WhitelistResponse {
1515 pub success: Option<bool>,
1516 #[serde(rename = "tradeAccount")]
1517 pub trade_account: Option<String>,
1518 #[serde(rename = "alreadyWhitelisted")]
1519 pub already_whitelisted: Option<bool>,
1520}
1521
1522#[derive(Debug, Clone, Serialize, Deserialize)]
1528pub struct FaucetResponse {
1529 pub message: Option<String>,
1530 pub error: Option<String>,
1531}
1532
1533#[derive(Debug, Clone, Serialize, Deserialize)]
1539pub struct ReferralInfo {
1540 pub valid: Option<bool>,
1541 #[serde(rename = "ownerAddress")]
1542 pub owner_address: Option<String>,
1543 #[serde(rename = "isActive")]
1544 pub is_active: Option<bool>,
1545}
1546
1547#[derive(Debug, Clone, Serialize, Deserialize)]
1553pub struct AggregatedAssetInfo {
1554 pub name: String,
1555 #[serde(deserialize_with = "deserialize_string_or_u64")]
1556 pub unified_cryptoasset_id: u64,
1557 pub can_withdraw: bool,
1558 pub can_deposit: bool,
1559 #[serde(deserialize_with = "deserialize_string_or_f64")]
1560 pub min_withdraw: f64,
1561 #[serde(deserialize_with = "deserialize_string_or_f64")]
1562 pub min_deposit: f64,
1563 #[serde(deserialize_with = "deserialize_string_or_f64")]
1564 pub maker_fee: f64,
1565 #[serde(deserialize_with = "deserialize_string_or_f64")]
1566 pub taker_fee: f64,
1567}
1568
1569pub type AggregatedAssets = BTreeMap<String, AggregatedAssetInfo>;
1571
1572#[derive(Debug, Clone, Serialize, Deserialize)]
1574pub struct AggregatedOrderbook {
1575 #[serde(deserialize_with = "deserialize_string_or_u64")]
1576 pub timestamp: u64,
1577 pub bids: Vec<[f64; 2]>,
1578 pub asks: Vec<[f64; 2]>,
1579}
1580
1581#[derive(Debug, Clone, Serialize, Deserialize)]
1583pub struct CoingeckoAggregatedOrderbook {
1584 pub ticker_id: String,
1585 #[serde(deserialize_with = "deserialize_string_or_u64")]
1586 pub timestamp: u64,
1587 pub bids: Vec<[f64; 2]>,
1588 pub asks: Vec<[f64; 2]>,
1589}
1590
1591#[derive(Debug, Clone, Serialize, Deserialize)]
1593pub struct PairSummary {
1594 pub trading_pairs: String,
1595 pub base_currency: String,
1596 pub quote_currency: String,
1597 #[serde(deserialize_with = "deserialize_string_or_f64")]
1598 pub last_price: f64,
1599 #[serde(deserialize_with = "deserialize_string_or_f64")]
1600 pub lowest_ask: f64,
1601 #[serde(deserialize_with = "deserialize_string_or_f64")]
1602 pub highest_bid: f64,
1603 #[serde(deserialize_with = "deserialize_string_or_f64")]
1604 pub base_volume: f64,
1605 #[serde(deserialize_with = "deserialize_string_or_f64")]
1606 pub quote_volume: f64,
1607 #[serde(deserialize_with = "deserialize_string_or_f64")]
1608 pub price_change_percent_24h: f64,
1609 #[serde(deserialize_with = "deserialize_string_or_f64")]
1610 pub highest_price_24h: f64,
1611 #[serde(deserialize_with = "deserialize_string_or_f64")]
1612 pub lowest_price_24h: f64,
1613}
1614
1615#[derive(Debug, Clone, Serialize, Deserialize)]
1617pub struct AggregatedTickerData {
1618 #[serde(deserialize_with = "deserialize_string_or_f64")]
1619 pub last_price: f64,
1620 #[serde(deserialize_with = "deserialize_string_or_f64")]
1621 pub base_volume: f64,
1622 #[serde(deserialize_with = "deserialize_string_or_f64")]
1623 pub quote_volume: f64,
1624}
1625
1626pub type AggregatedTicker = BTreeMap<String, AggregatedTickerData>;
1628
1629#[derive(Debug, Clone, Serialize, Deserialize)]
1631pub struct PairTicker {
1632 pub ticker_id: String,
1633 pub base_currency: String,
1634 pub target_currency: String,
1635 #[serde(deserialize_with = "deserialize_string_or_f64")]
1636 pub last_price: f64,
1637 #[serde(deserialize_with = "deserialize_string_or_f64")]
1638 pub base_volume: f64,
1639 #[serde(deserialize_with = "deserialize_string_or_f64")]
1640 pub target_volume: f64,
1641 #[serde(deserialize_with = "deserialize_string_or_f64")]
1642 pub bid: f64,
1643 #[serde(deserialize_with = "deserialize_string_or_f64")]
1644 pub ask: f64,
1645 #[serde(deserialize_with = "deserialize_string_or_f64")]
1646 pub high: f64,
1647 #[serde(deserialize_with = "deserialize_string_or_f64")]
1648 pub low: f64,
1649}
1650
1651#[derive(Debug, Clone, Serialize, Deserialize)]
1653pub struct AggregatedTrade {
1654 #[serde(deserialize_with = "deserialize_string_or_u64")]
1655 pub trade_id: u64,
1656 #[serde(deserialize_with = "deserialize_string_or_f64")]
1657 pub price: f64,
1658 #[serde(deserialize_with = "deserialize_string_or_f64")]
1659 pub base_volume: f64,
1660 #[serde(deserialize_with = "deserialize_string_or_f64")]
1661 pub quote_volume: f64,
1662 #[serde(deserialize_with = "deserialize_string_or_u64")]
1663 pub timestamp: u64,
1664 #[serde(rename = "type")]
1665 pub trade_type: String,
1666}
1667
1668#[derive(Debug, Clone, Serialize, Deserialize)]
1674pub struct OrderUpdate {
1675 pub action: String,
1676 #[serde(default)]
1677 pub orders: Vec<Order>,
1678 pub onchain_timestamp: Option<String>,
1679 pub seen_timestamp: String,
1680}
1681
1682#[derive(Debug, Clone, Serialize, Deserialize)]
1684pub struct TradeUpdate {
1685 pub action: String,
1686 #[serde(default)]
1687 pub trades: Vec<Trade>,
1688 pub market_id: MarketId,
1689 pub onchain_timestamp: Option<String>,
1690 pub seen_timestamp: String,
1691}
1692
1693#[derive(Debug, Clone, Serialize, Deserialize)]
1695pub struct BalanceEntry {
1696 pub identity: Identity,
1697 pub asset_id: AssetId,
1698 #[serde(deserialize_with = "deserialize_string_or_u128")]
1699 pub total_locked: u128,
1700 #[serde(deserialize_with = "deserialize_string_or_u128")]
1701 pub total_unlocked: u128,
1702 #[serde(deserialize_with = "deserialize_string_or_u128")]
1703 pub trading_account_balance: u128,
1704 pub order_books: HashMap<String, OrderBookBalance>,
1705}
1706
1707#[derive(Debug, Clone, Serialize, Deserialize)]
1709pub struct BalanceUpdate {
1710 pub action: String,
1711 #[serde(default)]
1712 pub balance: Vec<BalanceEntry>,
1713 pub onchain_timestamp: Option<String>,
1714 pub seen_timestamp: String,
1715}
1716
1717#[derive(Debug, Clone, Serialize, Deserialize)]
1719pub struct NonceUpdate {
1720 pub action: String,
1721 pub contract_id: TradeAccountId,
1722 #[serde(deserialize_with = "deserialize_string_or_u64")]
1723 pub nonce: u64,
1724 pub onchain_timestamp: Option<String>,
1725 pub seen_timestamp: String,
1726}
1727
1728#[derive(Debug, Clone, Serialize, Deserialize)]
1730pub struct WsMessage {
1731 pub action: Option<String>,
1732 #[serde(flatten)]
1733 pub extra: HashMap<String, serde_json::Value>,
1734}
1735
1736#[derive(Debug, Clone)]
1738pub struct TxResult {
1739 pub tx_id: String,
1740 pub orders: Vec<Order>,
1741}
1742
1743#[cfg(test)]
1744mod tests {
1745 use super::*;
1746
1747 fn sample_market() -> Market {
1748 Market {
1749 contract_id: ContractId::new(
1750 "0x1111111111111111111111111111111111111111111111111111111111111111",
1751 ),
1752 market_id: MarketId::new(
1753 "0x2222222222222222222222222222222222222222222222222222222222222222",
1754 ),
1755 whitelist_id: None,
1756 blacklist_id: None,
1757 maker_fee: 0,
1758 taker_fee: 0,
1759 min_order: 1,
1760 dust: 0,
1761 price_window: 0,
1762 base: MarketAsset {
1763 symbol: "BASE".to_string(),
1764 asset: AssetId::new(
1765 "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1766 ),
1767 decimals: 9,
1768 max_precision: 3,
1769 },
1770 quote: MarketAsset {
1771 symbol: "QUOTE".to_string(),
1772 asset: AssetId::new(
1773 "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1774 ),
1775 decimals: 9,
1776 max_precision: 4,
1777 },
1778 }
1779 }
1780
1781 #[test]
1782 fn market_price_accepts_valid_precision() {
1783 let market = sample_market();
1784 let price = market.price("12.3456").expect("price should be valid");
1785 assert_eq!(price.value(), "12.3456".parse().unwrap());
1786 market
1787 .validate_price_binding(&price)
1788 .expect("binding should match");
1789 }
1790
1791 #[test]
1792 fn market_price_rejects_excess_precision() {
1793 let market = sample_market();
1794 let err = market
1795 .price("12.34567")
1796 .expect_err("price precision should be rejected");
1797 assert!(matches!(err, O2Error::InvalidOrderParams(_)));
1798 }
1799
1800 #[test]
1801 fn market_quantity_rejects_excess_precision() {
1802 let market = sample_market();
1803 let err = market
1804 .quantity("1.2345")
1805 .expect_err("quantity precision should be rejected");
1806 assert!(matches!(err, O2Error::InvalidOrderParams(_)));
1807 }
1808
1809 #[test]
1810 fn market_quantity_binding_rejects_cross_market() {
1811 let market_a = sample_market();
1812 let mut market_b = sample_market();
1813 market_b.market_id =
1814 MarketId::new("0x3333333333333333333333333333333333333333333333333333333333333333");
1815
1816 let quantity = market_a
1817 .quantity("1.234")
1818 .expect("quantity should be valid");
1819 let err = market_b
1820 .validate_quantity_binding(&quantity)
1821 .expect_err("cross-market quantity must be rejected");
1822 assert!(format!("{err}").contains("stale or bound to a different market"));
1823 }
1824
1825 #[test]
1826 fn market_price_binding_rejects_precision_drift() {
1827 let market_a = sample_market();
1828 let mut market_b = sample_market();
1829 market_b.quote.max_precision = market_a.quote.max_precision + 1;
1830
1831 let price = market_a.price("1.2345").expect("price should be valid");
1832 let err = market_b
1833 .validate_price_binding(&price)
1834 .expect_err("precision drift should be rejected");
1835 assert!(format!("{err}").contains("stale or bound to a different market"));
1836 }
1837}