use rust_decimal::Decimal;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::str::FromStr;
use crate::decimal::UnsignedDecimal;
use crate::errors::O2Error;
macro_rules! newtype_id {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub(crate) fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for $name {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<&$name> for $name {
fn from(v: &$name) -> Self {
v.clone()
}
}
impl std::ops::Deref for $name {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl Default for $name {
fn default() -> Self {
Self(String::new())
}
}
};
}
pub trait IntoValidId<T> {
fn into_valid(self) -> Result<T, O2Error>;
}
fn validate_hex(type_name: &str, s: &str) -> Result<(), O2Error> {
let hex = s
.strip_prefix("0x")
.or_else(|| s.strip_prefix("0X"))
.unwrap_or(s);
if hex.is_empty() {
return Err(O2Error::Other(format!(
"{type_name}: requires a non-empty hex string, got {s:?}"
)));
}
if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(O2Error::Other(format!(
"{type_name}: contains non-hex characters: {s:?}"
)));
}
Ok(())
}
macro_rules! hex_id {
($(#[$meta:meta])* $name:ident) => {
newtype_id!($(#[$meta])* $name);
impl IntoValidId<$name> for &str {
fn into_valid(self) -> Result<$name, O2Error> {
validate_hex(stringify!($name), self)?;
Ok($name::new(self))
}
}
impl IntoValidId<$name> for String {
fn into_valid(self) -> Result<$name, O2Error> {
validate_hex(stringify!($name), &self)?;
Ok($name::new(self))
}
}
impl IntoValidId<$name> for $name {
fn into_valid(self) -> Result<$name, O2Error> {
Ok(self)
}
}
impl IntoValidId<$name> for &$name {
fn into_valid(self) -> Result<$name, O2Error> {
Ok(self.clone())
}
}
};
}
newtype_id!(
MarketSymbol
);
impl IntoValidId<MarketSymbol> for &str {
fn into_valid(self) -> Result<MarketSymbol, O2Error> {
MarketSymbol::parse(self)
}
}
impl IntoValidId<MarketSymbol> for String {
fn into_valid(self) -> Result<MarketSymbol, O2Error> {
MarketSymbol::parse(self)
}
}
impl IntoValidId<MarketSymbol> for MarketSymbol {
fn into_valid(self) -> Result<MarketSymbol, O2Error> {
Ok(self)
}
}
impl IntoValidId<MarketSymbol> for &MarketSymbol {
fn into_valid(self) -> Result<MarketSymbol, O2Error> {
Ok(self.clone())
}
}
impl MarketSymbol {
pub fn parse(input: impl AsRef<str>) -> Result<Self, O2Error> {
Self::from_str(input.as_ref())
}
}
impl FromStr for MarketSymbol {
type Err = O2Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(O2Error::InvalidRequest(
"Market symbol cannot be empty".to_string(),
));
}
let (base_raw, quote_raw) = trimmed.split_once('/').ok_or_else(|| {
O2Error::InvalidRequest(format!(
"Invalid market symbol '{trimmed}'. Expected format BASE/QUOTE"
))
})?;
if quote_raw.contains('/') {
return Err(O2Error::InvalidRequest(format!(
"Invalid market symbol '{trimmed}'. Expected exactly one '/' separator"
)));
}
let base = base_raw.trim();
let quote = quote_raw.trim();
if base.is_empty() || quote.is_empty() {
return Err(O2Error::InvalidRequest(format!(
"Invalid market symbol '{trimmed}'. Base and quote must be non-empty"
)));
}
Ok(MarketSymbol::new(format!("{base}/{quote}")))
}
}
pub trait IntoMarketSymbol {
fn into_market_symbol(self) -> Result<MarketSymbol, O2Error>;
}
impl IntoMarketSymbol for MarketSymbol {
fn into_market_symbol(self) -> Result<MarketSymbol, O2Error> {
MarketSymbol::parse(self.as_str())
}
}
impl IntoMarketSymbol for &MarketSymbol {
fn into_market_symbol(self) -> Result<MarketSymbol, O2Error> {
MarketSymbol::parse(self.as_str())
}
}
impl IntoMarketSymbol for &str {
fn into_market_symbol(self) -> Result<MarketSymbol, O2Error> {
MarketSymbol::parse(self)
}
}
impl IntoMarketSymbol for String {
fn into_market_symbol(self) -> Result<MarketSymbol, O2Error> {
MarketSymbol::parse(self)
}
}
impl IntoMarketSymbol for &String {
fn into_market_symbol(self) -> Result<MarketSymbol, O2Error> {
MarketSymbol::parse(self)
}
}
hex_id!(
ContractId
);
hex_id!(
MarketId
);
hex_id!(
OrderId
);
hex_id!(
TradeId
);
hex_id!(
TradeAccountId
);
hex_id!(
AssetId
);
fn normalize_hex_prefixed(s: String) -> String {
if s.starts_with("0x") || s.starts_with("0X") || s.is_empty() {
s
} else {
format!("0x{s}")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Default)]
#[serde(transparent)]
pub struct TxId(String);
impl TxId {
pub fn new(s: impl Into<String>) -> Self {
Self(normalize_hex_prefixed(s.into()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for TxId {
fn as_ref(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for TxId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for TxId {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<&str> for TxId {
fn from(s: &str) -> Self {
Self::new(s)
}
}
impl std::ops::Deref for TxId {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for TxId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
Ok(TxId::new(raw))
}
}
fn deserialize_string_or_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
use serde::de;
struct StringOrU64;
impl<'de> de::Visitor<'de> for StringOrU64 {
type Value = u64;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a u64 or a string containing a decimal/0x-hex u64")
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<u64, E> {
Ok(v)
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<u64, E> {
u64::try_from(v).map_err(de::Error::custom)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<u64, E> {
if let Some(hex) = v.strip_prefix("0x").or_else(|| v.strip_prefix("0X")) {
u64::from_str_radix(hex, 16).map_err(de::Error::custom)
} else {
v.parse().map_err(de::Error::custom)
}
}
}
deserializer.deserialize_any(StringOrU64)
}
fn deserialize_optional_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
match value {
Some(serde_json::Value::String(s)) => {
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u64::from_str_radix(hex, 16)
.map(Some)
.map_err(serde::de::Error::custom)
} else {
s.parse().map(Some).map_err(serde::de::Error::custom)
}
}
Some(serde_json::Value::Number(n)) => n
.as_u64()
.ok_or_else(|| serde::de::Error::custom("number is not u64"))
.map(Some),
Some(serde_json::Value::Null) | None => Ok(None),
Some(v) => Err(serde::de::Error::custom(format!(
"expected string/number/null for u64 field, got {v}"
))),
}
}
fn deserialize_string_or_u128<'de, D>(deserializer: D) -> Result<u128, D::Error>
where
D: Deserializer<'de>,
{
use serde::de;
struct StringOrU128;
impl<'de> de::Visitor<'de> for StringOrU128 {
type Value = u128;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("a u128 or a string containing a decimal u128")
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<u128, E> {
Ok(v as u128)
}
fn visit_u128<E: de::Error>(self, v: u128) -> Result<u128, E> {
Ok(v)
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<u128, E> {
u128::try_from(v).map_err(de::Error::custom)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<u128, E> {
v.parse().map_err(de::Error::custom)
}
}
deserializer.deserialize_any(StringOrU128)
}
fn deserialize_string_or_f64<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: Deserializer<'de>,
{
use serde::de;
struct StringOrF64;
impl<'de> de::Visitor<'de> for StringOrF64 {
type Value = f64;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("an f64 or a string containing an f64")
}
fn visit_f64<E: de::Error>(self, v: f64) -> Result<f64, E> {
Ok(v)
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<f64, E> {
Ok(v as f64)
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<f64, E> {
Ok(v as f64)
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<f64, E> {
v.parse().map_err(de::Error::custom)
}
}
deserializer.deserialize_any(StringOrF64)
}
fn deserialize_optional_f64<'de, D>(deserializer: D) -> Result<Option<f64>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
match value {
Some(serde_json::Value::String(s)) => s.parse().map(Some).map_err(serde::de::Error::custom),
Some(serde_json::Value::Number(n)) => n
.as_f64()
.ok_or_else(|| serde::de::Error::custom("number is not f64"))
.map(Some),
Some(serde_json::Value::Null) | None => Ok(None),
Some(v) => Err(serde::de::Error::custom(format!(
"expected string/number/null for f64 field, got {v}"
))),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Side {
Buy,
Sell,
}
impl Side {
pub fn as_str(&self) -> &str {
match self {
Side::Buy => "Buy",
Side::Sell => "Sell",
}
}
}
impl std::fmt::Display for Side {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl Serialize for Side {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Side {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = String::deserialize(deserializer)?;
match raw.to_ascii_lowercase().as_str() {
"buy" => Ok(Side::Buy),
"sell" => Ok(Side::Sell),
_ => Err(serde::de::Error::custom(format!("invalid side '{raw}'"))),
}
}
}
#[derive(Debug, Clone)]
pub enum OrderType {
Spot,
Market,
FillOrKill,
PostOnly,
Limit {
price: UnsignedDecimal,
timestamp: u64,
},
BoundedMarket {
max_price: UnsignedDecimal,
min_price: UnsignedDecimal,
},
}
#[derive(Debug, Clone)]
pub enum Action {
CreateOrder {
side: Side,
price: UnsignedDecimal,
quantity: UnsignedDecimal,
order_type: OrderType,
},
CancelOrder {
order_id: OrderId,
},
SettleBalance,
RegisterReferer {
to: Identity,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Price {
value: UnsignedDecimal,
market_id: MarketId,
quote_decimals: u32,
quote_max_precision: u32,
}
impl Price {
pub fn value(&self) -> UnsignedDecimal {
self.value
}
pub fn market_id(&self) -> &MarketId {
&self.market_id
}
}
impl std::fmt::Display for Price {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.value.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Quantity {
value: UnsignedDecimal,
market_id: MarketId,
base_decimals: u32,
base_max_precision: u32,
}
impl Quantity {
pub fn value(&self) -> UnsignedDecimal {
self.value
}
pub fn market_id(&self) -> &MarketId {
&self.market_id
}
}
impl std::fmt::Display for Quantity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.value.fmt(f)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OrderPriceInput {
Unchecked(UnsignedDecimal),
Checked(Price),
}
impl TryFrom<UnsignedDecimal> for OrderPriceInput {
type Error = O2Error;
fn try_from(value: UnsignedDecimal) -> Result<Self, Self::Error> {
Ok(Self::Unchecked(value))
}
}
impl TryFrom<Price> for OrderPriceInput {
type Error = O2Error;
fn try_from(value: Price) -> Result<Self, Self::Error> {
Ok(Self::Checked(value))
}
}
impl TryFrom<&str> for OrderPriceInput {
type Error = O2Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(Self::Unchecked(value.parse()?))
}
}
impl TryFrom<String> for OrderPriceInput {
type Error = O2Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(Self::Unchecked(value.parse()?))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OrderQuantityInput {
Unchecked(UnsignedDecimal),
Checked(Quantity),
}
impl TryFrom<UnsignedDecimal> for OrderQuantityInput {
type Error = O2Error;
fn try_from(value: UnsignedDecimal) -> Result<Self, Self::Error> {
Ok(Self::Unchecked(value))
}
}
impl TryFrom<Quantity> for OrderQuantityInput {
type Error = O2Error;
fn try_from(value: Quantity) -> Result<Self, Self::Error> {
Ok(Self::Checked(value))
}
}
impl TryFrom<&str> for OrderQuantityInput {
type Error = O2Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Ok(Self::Unchecked(value.parse()?))
}
}
impl TryFrom<String> for OrderQuantityInput {
type Error = O2Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(Self::Unchecked(value.parse()?))
}
}
impl OrderType {
pub fn to_encoding(
&self,
market: &Market,
) -> Result<(crate::encoding::OrderTypeEncoding, serde_json::Value), O2Error> {
use crate::encoding::OrderTypeEncoding;
match self {
OrderType::Spot => Ok((OrderTypeEncoding::Spot, serde_json::json!("Spot"))),
OrderType::Market => Ok((OrderTypeEncoding::Market, serde_json::json!("Market"))),
OrderType::FillOrKill => Ok((
OrderTypeEncoding::FillOrKill,
serde_json::json!("FillOrKill"),
)),
OrderType::PostOnly => Ok((OrderTypeEncoding::PostOnly, serde_json::json!("PostOnly"))),
OrderType::Limit { price, timestamp } => {
let scaled_price = market.scale_price(price)?;
Ok((
OrderTypeEncoding::Limit {
price: scaled_price,
timestamp: *timestamp,
},
serde_json::json!({ "Limit": [scaled_price.to_string(), timestamp.to_string()] }),
))
}
OrderType::BoundedMarket {
max_price,
min_price,
} => {
let scaled_max = market.scale_price(max_price)?;
let scaled_min = market.scale_price(min_price)?;
Ok((
OrderTypeEncoding::BoundedMarket {
max_price: scaled_max,
min_price: scaled_min,
},
serde_json::json!({ "BoundedMarket": { "max_price": scaled_max.to_string(), "min_price": scaled_min.to_string() } }),
))
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Identity {
Address(String),
ContractId(String),
}
impl Identity {
pub fn address_value(&self) -> &str {
match self {
Identity::Address(a) => a,
Identity::ContractId(c) => c,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Signature {
Secp256k1(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketAsset {
pub symbol: String,
pub asset: AssetId,
pub decimals: u32,
pub max_precision: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Market {
pub contract_id: ContractId,
pub market_id: MarketId,
pub whitelist_id: Option<ContractId>,
pub blacklist_id: Option<ContractId>,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub maker_fee: u64,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub taker_fee: u64,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub min_order: u64,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub dust: u64,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub price_window: u64,
pub base: MarketAsset,
pub quote: MarketAsset,
}
impl Market {
fn parsed_unsigned(value: &str, field: &str) -> Result<UnsignedDecimal, O2Error> {
UnsignedDecimal::from_str(value)
.map_err(|e| O2Error::InvalidOrderParams(format!("Invalid {field}: {e}")))
}
fn decimal_scale(value: &UnsignedDecimal) -> u32 {
value.inner().normalize().scale()
}
pub fn price(&self, value: &str) -> Result<Price, O2Error> {
let parsed = Self::parsed_unsigned(value, "price")?;
self.price_from_decimal(parsed)
}
pub fn price_from_decimal(&self, value: UnsignedDecimal) -> Result<Price, O2Error> {
let scale = Self::decimal_scale(&value);
if scale > self.quote.max_precision {
return Err(O2Error::InvalidOrderParams(format!(
"Price precision {} exceeds max {} for market {}",
scale, self.quote.max_precision, self.market_id
)));
}
let _ = self.scale_price(&value)?;
Ok(Price {
value,
market_id: self.market_id.clone(),
quote_decimals: self.quote.decimals,
quote_max_precision: self.quote.max_precision,
})
}
pub fn quantity(&self, value: &str) -> Result<Quantity, O2Error> {
let parsed = Self::parsed_unsigned(value, "quantity")?;
self.quantity_from_decimal(parsed)
}
pub fn quantity_from_decimal(&self, value: UnsignedDecimal) -> Result<Quantity, O2Error> {
let scale = Self::decimal_scale(&value);
if scale > self.base.max_precision {
return Err(O2Error::InvalidOrderParams(format!(
"Quantity precision {} exceeds max {} for market {}",
scale, self.base.max_precision, self.market_id
)));
}
let _ = self.scale_quantity(&value)?;
Ok(Quantity {
value,
market_id: self.market_id.clone(),
base_decimals: self.base.decimals,
base_max_precision: self.base.max_precision,
})
}
pub fn validate_price_binding(&self, price: &Price) -> Result<(), O2Error> {
if price.market_id != self.market_id
|| price.quote_decimals != self.quote.decimals
|| price.quote_max_precision != self.quote.max_precision
{
return Err(O2Error::Other(format!(
"Price wrapper is stale or bound to a different market (expected {}, got {})",
self.market_id, price.market_id
)));
}
Ok(())
}
pub fn validate_quantity_binding(&self, quantity: &Quantity) -> Result<(), O2Error> {
if quantity.market_id != self.market_id
|| quantity.base_decimals != self.base.decimals
|| quantity.base_max_precision != self.base.max_precision
{
return Err(O2Error::Other(format!(
"Quantity wrapper is stale or bound to a different market (expected {}, got {})",
self.market_id, quantity.market_id
)));
}
Ok(())
}
fn checked_pow_u64(exp: u32, field: &str) -> Result<u64, O2Error> {
10u64
.checked_pow(exp)
.ok_or_else(|| O2Error::Other(format!("Invalid {field}: 10^{exp} overflows u64")))
}
fn checked_pow_u128(exp: u32, field: &str) -> Result<u128, O2Error> {
10u128
.checked_pow(exp)
.ok_or_else(|| O2Error::Other(format!("Invalid {field}: 10^{exp} overflows u128")))
}
fn checked_truncate_factor(
decimals: u32,
max_precision: u32,
field: &str,
) -> Result<u64, O2Error> {
if max_precision > decimals {
return Err(O2Error::Other(format!(
"Invalid {field}: max_precision ({max_precision}) exceeds decimals ({decimals})"
)));
}
Self::checked_pow_u64(decimals - max_precision, field)
}
pub fn format_price(&self, chain_value: u64) -> UnsignedDecimal {
let factor = 10u64.pow(self.quote.decimals);
let d = Decimal::from(chain_value) / Decimal::from(factor);
UnsignedDecimal::new(d).unwrap()
}
pub fn scale_price(&self, human_value: &UnsignedDecimal) -> Result<u64, O2Error> {
let factor_u64 = Self::checked_pow_u64(self.quote.decimals, "quote.decimals")?;
let factor = Decimal::from(factor_u64);
let scaled_str = (*human_value.inner() * factor).floor().to_string();
let scaled = scaled_str.parse::<u64>().map_err(|e| {
O2Error::Other(format!(
"Failed to scale price '{}' into u64: {e}",
human_value
))
})?;
let truncate_factor = Self::checked_truncate_factor(
self.quote.decimals,
self.quote.max_precision,
"quote precision",
)?;
Ok((scaled / truncate_factor) * truncate_factor)
}
pub fn format_quantity(&self, chain_value: u64) -> UnsignedDecimal {
let factor = 10u64.pow(self.base.decimals);
let d = Decimal::from(chain_value) / Decimal::from(factor);
UnsignedDecimal::new(d).unwrap()
}
pub fn scale_quantity(&self, human_value: &UnsignedDecimal) -> Result<u64, O2Error> {
let factor_u64 = Self::checked_pow_u64(self.base.decimals, "base.decimals")?;
let factor = Decimal::from(factor_u64);
let scaled_str = (*human_value.inner() * factor).floor().to_string();
let scaled = scaled_str.parse::<u64>().map_err(|e| {
O2Error::Other(format!(
"Failed to scale quantity '{}' into u64: {e}",
human_value
))
})?;
let truncate_factor = Self::checked_truncate_factor(
self.base.decimals,
self.base.max_precision,
"base precision",
)?;
Ok((scaled / truncate_factor) * truncate_factor)
}
pub fn symbol_pair(&self) -> MarketSymbol {
MarketSymbol::new(format!("{}/{}", self.base.symbol, self.quote.symbol))
}
pub fn adjust_quantity(&self, price: u64, quantity: u64) -> Result<u64, O2Error> {
if price == 0 {
return Err(O2Error::InvalidOrderParams(
"Price cannot be zero when adjusting quantity".into(),
));
}
let base_factor = Self::checked_pow_u128(self.base.decimals, "base.decimals")?;
let product = price as u128 * quantity as u128;
let remainder = product % base_factor;
if remainder == 0 {
return Ok(quantity);
}
let adjusted_product = product - remainder;
let adjusted = adjusted_product / price as u128;
if adjusted > u64::MAX as u128 {
return Err(O2Error::InvalidOrderParams(
"Adjusted quantity exceeds u64 range".into(),
));
}
Ok(adjusted as u64)
}
pub fn validate_order(&self, price: u64, quantity: u64) -> Result<(), O2Error> {
let base_factor = Self::checked_pow_u128(self.base.decimals, "base.decimals")?;
let quote_value = (price as u128 * quantity as u128) / base_factor;
let min_order: u128 = self.min_order as u128;
if quote_value < min_order {
return Err(O2Error::InvalidOrderParams(format!(
"Quote value {} below min_order {}",
quote_value, min_order
)));
}
if (price as u128 * quantity as u128) % base_factor != 0 {
return Err(O2Error::InvalidOrderParams(
"FractionalPrice: (price * quantity) % 10^base_decimals != 0".into(),
));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketsResponse {
pub books_registry_id: ContractId,
pub books_whitelist_id: Option<ContractId>,
pub books_blacklist_id: Option<ContractId>,
pub accounts_registry_id: ContractId,
pub trade_account_oracle_id: ContractId,
pub fast_bridge_asset_registry_contract_id: Option<ContractId>,
pub chain_id: String,
pub base_asset_id: AssetId,
pub markets: Vec<Market>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketSummary {
pub market_id: MarketId,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub high_price: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub low_price: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub last_price: Option<u64>,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub volume_24h: u128,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub change_24h: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MarketTicker {
pub market_id: MarketId,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub high: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub low: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub bid: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub bid_volume: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub ask: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub ask_volume: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub open: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub close: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub last: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub previous_close: Option<u64>,
#[serde(default, deserialize_with = "deserialize_optional_f64")]
pub change: Option<f64>,
#[serde(default, deserialize_with = "deserialize_optional_f64")]
pub percentage: Option<f64>,
#[serde(default, deserialize_with = "deserialize_optional_f64")]
pub average: Option<f64>,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub base_volume: u128,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub quote_volume: u128,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub timestamp: u128,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeAccount {
#[serde(default)]
pub last_modification: u64,
#[serde(default, deserialize_with = "deserialize_string_or_u64")]
pub nonce: u64,
pub owner: Identity,
#[serde(default)]
pub synced_with_network: Option<bool>,
#[serde(default)]
pub sync_state: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccountResponse {
pub trade_account_id: Option<TradeAccountId>,
pub trade_account: Option<TradeAccount>,
pub session: Option<SessionInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
pub session_id: Identity,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub expiry: u64,
pub contract_ids: Vec<ContractId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateAccountResponse {
pub trade_account_id: TradeAccountId,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub nonce: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionRequest {
pub contract_id: TradeAccountId,
pub session_id: Identity,
pub signature: Signature,
pub contract_ids: Vec<ContractId>,
pub nonce: String,
pub expiry: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionResponse {
pub tx_id: TxId,
pub trade_account_id: TradeAccountId,
pub contract_ids: Vec<ContractId>,
pub session_id: Identity,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub session_expiry: u64,
}
#[derive(Debug, Clone)]
pub struct Session {
pub owner_address: [u8; 32],
pub session_private_key: [u8; 32],
pub session_address: [u8; 32],
pub trade_account_id: TradeAccountId,
pub contract_ids: Vec<ContractId>,
pub expiry: u64,
pub nonce: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Order {
#[serde(default)]
pub order_id: OrderId,
pub side: Side,
pub order_type: serde_json::Value,
#[serde(default, deserialize_with = "deserialize_string_or_u64")]
pub quantity: u64,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub quantity_fill: Option<u64>,
#[serde(default, deserialize_with = "deserialize_string_or_u64")]
pub price: u64,
#[serde(default, deserialize_with = "deserialize_optional_u64")]
pub price_fill: Option<u64>,
pub timestamp: Option<serde_json::Value>,
#[serde(default)]
pub close: bool,
#[serde(default)]
pub partially_filled: bool,
#[serde(default)]
pub cancel: bool,
#[serde(default)]
pub desired_quantity: Option<serde_json::Value>,
#[serde(default)]
pub base_decimals: Option<u32>,
#[serde(default)]
pub account: Option<Identity>,
#[serde(default)]
pub fill: Option<serde_json::Value>,
#[serde(default)]
pub order_tx_history: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub market_id: Option<MarketId>,
#[serde(default)]
pub owner: Option<Identity>,
#[serde(default)]
pub history: Option<Vec<serde_json::Value>>,
#[serde(default)]
pub fills: Option<Vec<serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrdersResponse {
pub identity: Identity,
pub market_id: MarketId,
#[serde(default)]
pub orders: Vec<Order>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TraderSide {
Maker,
Taker,
Both,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Trade {
pub trade_id: TradeId,
pub side: Side,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub total: u128,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub quantity: u64,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub price: u64,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub timestamp: u128,
#[serde(default)]
pub trader_side: Option<TraderSide>,
#[serde(default)]
pub maker: Option<Identity>,
#[serde(default)]
pub taker: Option<Identity>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradesResponse {
#[serde(default)]
pub trades: Vec<Trade>,
pub market_id: MarketId,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderBookBalance {
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub locked: u128,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub unlocked: u128,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub fee: u128,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BalanceResponse {
pub order_books: HashMap<String, OrderBookBalance>,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub total_locked: u128,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub total_unlocked: u128,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub trading_account_balance: u128,
}
impl BalanceResponse {
#[inline]
pub fn available(&self) -> u128 {
self.total_unlocked
}
#[inline]
pub fn locked(&self) -> u128 {
self.total_locked
}
#[inline]
pub fn total(&self) -> u128 {
self.total_unlocked + self.total_locked
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepthLevel {
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub price: u64,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub quantity: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepthSnapshot {
#[serde(default, rename = "buys")]
pub bids: Vec<DepthLevel>,
#[serde(default, rename = "sells")]
pub asks: Vec<DepthLevel>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepthUpdate {
pub action: String,
pub changes: Option<DepthSnapshot>,
#[serde(alias = "view")]
pub view: Option<DepthSnapshot>,
pub market_id: MarketId,
pub onchain_timestamp: Option<String>,
pub seen_timestamp: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bar {
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub open: u64,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub high: u64,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub low: u64,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub close: u64,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub buy_volume: u128,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub sell_volume: u128,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub timestamp: u128,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateOrderAction {
pub side: String,
pub price: String,
pub quantity: String,
pub order_type: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelOrderAction {
pub order_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SettleBalanceAction {
pub to: Identity,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ActionItem {
CreateOrder {
#[serde(rename = "CreateOrder")]
create_order: CreateOrderAction,
},
CancelOrder {
#[serde(rename = "CancelOrder")]
cancel_order: CancelOrderAction,
},
SettleBalance {
#[serde(rename = "SettleBalance")]
settle_balance: SettleBalanceAction,
},
RegisterReferer {
#[serde(rename = "RegisterReferer")]
register_referer: SettleBalanceAction,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct MarketActions {
pub market_id: MarketId,
pub actions: Vec<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct SessionActionsRequest {
pub actions: Vec<MarketActions>,
pub signature: Signature,
pub nonce: String,
pub trade_account_id: TradeAccountId,
pub session_id: Identity,
#[serde(skip_serializing_if = "Option::is_none")]
pub collect_orders: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub variable_outputs: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionActionsResponse {
pub tx_id: Option<TxId>,
pub orders: Option<Vec<Order>>,
pub code: Option<u32>,
pub message: Option<String>,
pub reason: Option<String>,
pub receipts: Option<serde_json::Value>,
}
impl SessionActionsResponse {
pub fn is_success(&self) -> bool {
self.tx_id.is_some()
}
pub fn is_preflight_error(&self) -> bool {
self.code.is_some() && self.tx_id.is_none()
}
pub fn is_onchain_error(&self) -> bool {
self.message.is_some() && self.code.is_none() && self.tx_id.is_none()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WithdrawRequest {
pub trade_account_id: TradeAccountId,
pub signature: Signature,
pub nonce: String,
pub to: Identity,
pub asset_id: AssetId,
pub amount: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WithdrawResponse {
pub tx_id: Option<String>,
pub code: Option<u32>,
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhitelistRequest {
#[serde(rename = "tradeAccount")]
pub trade_account: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WhitelistResponse {
pub success: Option<bool>,
#[serde(rename = "tradeAccount")]
pub trade_account: Option<String>,
#[serde(rename = "alreadyWhitelisted")]
pub already_whitelisted: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FaucetResponse {
pub message: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReferralInfo {
pub valid: Option<bool>,
#[serde(rename = "ownerAddress")]
pub owner_address: Option<String>,
#[serde(rename = "isActive")]
pub is_active: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregatedAssetInfo {
pub name: String,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub unified_cryptoasset_id: u64,
pub can_withdraw: bool,
pub can_deposit: bool,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub min_withdraw: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub min_deposit: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub maker_fee: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub taker_fee: f64,
}
pub type AggregatedAssets = BTreeMap<String, AggregatedAssetInfo>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregatedOrderbook {
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub timestamp: u64,
pub bids: Vec<[f64; 2]>,
pub asks: Vec<[f64; 2]>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoingeckoAggregatedOrderbook {
pub ticker_id: String,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub timestamp: u64,
pub bids: Vec<[f64; 2]>,
pub asks: Vec<[f64; 2]>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairSummary {
pub trading_pairs: String,
pub base_currency: String,
pub quote_currency: String,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub last_price: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub lowest_ask: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub highest_bid: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub base_volume: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub quote_volume: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub price_change_percent_24h: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub highest_price_24h: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub lowest_price_24h: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregatedTickerData {
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub last_price: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub base_volume: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub quote_volume: f64,
}
pub type AggregatedTicker = BTreeMap<String, AggregatedTickerData>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairTicker {
pub ticker_id: String,
pub base_currency: String,
pub target_currency: String,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub last_price: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub base_volume: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub target_volume: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub bid: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub ask: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub high: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub low: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregatedTrade {
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub trade_id: u64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub price: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub base_volume: f64,
#[serde(deserialize_with = "deserialize_string_or_f64")]
pub quote_volume: f64,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub timestamp: u64,
#[serde(rename = "type")]
pub trade_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderUpdate {
pub action: String,
#[serde(default)]
pub orders: Vec<Order>,
pub onchain_timestamp: Option<String>,
pub seen_timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeUpdate {
pub action: String,
#[serde(default)]
pub trades: Vec<Trade>,
pub market_id: MarketId,
pub onchain_timestamp: Option<String>,
pub seen_timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BalanceEntry {
pub identity: Identity,
pub asset_id: AssetId,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub total_locked: u128,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub total_unlocked: u128,
#[serde(deserialize_with = "deserialize_string_or_u128")]
pub trading_account_balance: u128,
pub order_books: HashMap<String, OrderBookBalance>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BalanceUpdate {
pub action: String,
#[serde(default)]
pub balance: Vec<BalanceEntry>,
pub onchain_timestamp: Option<String>,
pub seen_timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NonceUpdate {
pub action: String,
pub contract_id: TradeAccountId,
#[serde(deserialize_with = "deserialize_string_or_u64")]
pub nonce: u64,
pub onchain_timestamp: Option<String>,
pub seen_timestamp: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WsMessage {
pub action: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct TxResult {
pub tx_id: String,
pub orders: Vec<Order>,
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_market() -> Market {
Market {
contract_id: ContractId::new(
"0x1111111111111111111111111111111111111111111111111111111111111111",
),
market_id: MarketId::new(
"0x2222222222222222222222222222222222222222222222222222222222222222",
),
whitelist_id: None,
blacklist_id: None,
maker_fee: 0,
taker_fee: 0,
min_order: 1,
dust: 0,
price_window: 0,
base: MarketAsset {
symbol: "BASE".to_string(),
asset: AssetId::new(
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
),
decimals: 9,
max_precision: 3,
},
quote: MarketAsset {
symbol: "QUOTE".to_string(),
asset: AssetId::new(
"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
),
decimals: 9,
max_precision: 4,
},
}
}
#[test]
fn market_price_accepts_valid_precision() {
let market = sample_market();
let price = market.price("12.3456").expect("price should be valid");
assert_eq!(price.value(), "12.3456".parse().unwrap());
market
.validate_price_binding(&price)
.expect("binding should match");
}
#[test]
fn market_price_rejects_excess_precision() {
let market = sample_market();
let err = market
.price("12.34567")
.expect_err("price precision should be rejected");
assert!(matches!(err, O2Error::InvalidOrderParams(_)));
}
#[test]
fn market_quantity_rejects_excess_precision() {
let market = sample_market();
let err = market
.quantity("1.2345")
.expect_err("quantity precision should be rejected");
assert!(matches!(err, O2Error::InvalidOrderParams(_)));
}
#[test]
fn market_quantity_binding_rejects_cross_market() {
let market_a = sample_market();
let mut market_b = sample_market();
market_b.market_id =
MarketId::new("0x3333333333333333333333333333333333333333333333333333333333333333");
let quantity = market_a
.quantity("1.234")
.expect("quantity should be valid");
let err = market_b
.validate_quantity_binding(&quantity)
.expect_err("cross-market quantity must be rejected");
assert!(format!("{err}").contains("stale or bound to a different market"));
}
#[test]
fn market_price_binding_rejects_precision_drift() {
let market_a = sample_market();
let mut market_b = sample_market();
market_b.quote.max_precision = market_a.quote.max_precision + 1;
let price = market_a.price("1.2345").expect("price should be valid");
let err = market_b
.validate_price_binding(&price)
.expect_err("precision drift should be rejected");
assert!(format!("{err}").contains("stale or bound to a different market"));
}
}