use crate::deploy::slot::{MarketSlot, binary_options_slot};
use crate::utils::AssetId;
use crate::utils::MarketKind;
use crate::utils::ids::WIDTH_48;
use crate::utils::json::Json;
use crate::utils::sign::goldilocks::Fp;
use crate::utils::sign::tx::{BadTx, Envelope, FEE_TICK, Tx, TxType, byte_list, element, number};
pub const MAX_EXTENSION_MULTIPLIER: u64 = (1 << 56) - 1;
pub const MAX_OPEN_INTEREST: u64 = (1 << 56) - 1;
pub const MAX_ORDER_BASE_AMOUNT: u64 = WIDTH_48;
pub const MAX_ORDER_QUOTE_AMOUNT: u64 = WIDTH_48;
pub const SYMBOL_BYTES: usize = 32;
const QUOTE_PLACES: u32 = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Settlement {
#[default]
Discrete,
Continuous,
}
impl Settlement {
#[must_use]
pub const fn code(self) -> u8 {
match self {
Settlement::Discrete => 0,
Settlement::Continuous => 1,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateMarket {
pub slot: MarketSlot,
pub start_ms: u64,
pub end_ms: u64,
pub settlement_cap: u32,
pub settlement: Settlement,
pub default_price: u32,
pub taker_fee: u32,
pub maker_fee: u32,
pub min_base_amount: u64,
pub min_quote_amount: u64,
pub order_quote_limit: u64,
pub open_interest_limit: u64,
pub frozen: bool,
pub symbol: String,
pub size_decimals: u8,
pub price_decimals: u8,
}
impl CreateMarket {
#[must_use]
pub fn quote_extension_multiplier(&self) -> u64 {
let used = u32::from(self.size_decimals) + u32::from(self.price_decimals);
QUOTE_PLACES.checked_sub(used).and_then(|exponent| 10u64.checked_pow(exponent)).unwrap_or(0)
}
#[must_use]
pub fn size_extension_multiplier(&self) -> Option<u64> {
u64::from(self.settlement_cap).checked_mul(self.quote_extension_multiplier())
}
#[must_use]
pub fn symbol_bytes(&self) -> [u8; SYMBOL_BYTES] {
let mut bytes = [0u8; SYMBOL_BYTES];
for (slot, byte) in bytes.iter_mut().zip(self.symbol.bytes()) {
*slot = byte;
}
bytes
}
}
fn collateral_id() -> u64 {
u64::from(AssetId::COLLATERAL.get())
}
pub(crate) fn check_window(start_ms: u64, end_ms: u64) -> Result<(), BadTx> {
if start_ms >= end_ms || end_ms > WIDTH_48 {
return Err(BadTx::new("StartTimestamp", "a window starts before it ends, which is before 2^48 ms"));
}
Ok(())
}
pub(crate) fn check_fees(taker_fee: u32, maker_fee: u32) -> Result<(), BadTx> {
if taker_fee > FEE_TICK || maker_fee > FEE_TICK {
return Err(BadTx::new("TakerFee", "a market fee is at most 1,000,000, all of the notional"));
}
Ok(())
}
pub(crate) fn check_limits(
min_base_amount: u64,
min_quote_amount: u64,
order_quote_limit: u64,
open_interest_limit: u64,
kind: MarketKind,
) -> Result<(), BadTx> {
if min_base_amount == 0 || min_base_amount > MAX_ORDER_BASE_AMOUNT {
return Err(BadTx::new("MinBaseAmount", "the smallest order is 1 to 2^48 - 1 size ticks"));
}
if min_quote_amount == 0 || min_quote_amount > MAX_ORDER_QUOTE_AMOUNT {
return Err(BadTx::new("MinQuoteAmount", "the smallest order's worth is 1 to 2^48 - 1"));
}
if order_quote_limit > MAX_ORDER_QUOTE_AMOUNT || order_quote_limit < min_quote_amount {
return Err(BadTx::new("OrderQuoteLimit", "the largest order's worth is the smallest's to 2^48 - 1"));
}
match kind {
MarketKind::Spot if open_interest_limit != 0 => {
Err(BadTx::new("OpenInterestLimit", "a spot book carries no open interest limit: 0"))
}
MarketKind::Spot => Ok(()),
_ if open_interest_limit < order_quote_limit || open_interest_limit > MAX_OPEN_INTEREST => {
Err(BadTx::new("OpenInterestLimit", "the open interest limit is the largest order's to 2^56 - 1"))
}
_ => Ok(()),
}
}
fn check_multiplier(multiplier: Option<u64>, field: &'static str) -> Result<u64, BadTx> {
match multiplier {
Some(value) if value != 0 && value <= MAX_EXTENSION_MULTIPLIER && value % u64::from(FEE_TICK) == 0 => Ok(value),
_ => Err(BadTx::new(field, "a multiplier is a multiple of 10^6 below 2^56: decimals add up to six at most")),
}
}
impl Tx for CreateMarket {
fn tx_type(&self) -> TxType {
TxType::BinaryOptionsCreateMarket
}
fn check_signing(&self, chain_id: u32, _envelope: &Envelope) -> Result<(), BadTx> {
crate::deploy::check_chain(chain_id)
}
fn hash_fields(&self) -> Vec<Fp> {
vec![
element(self.slot.get()),
element(MarketKind::BinaryOptions.code()),
element(collateral_id()),
element(collateral_id()),
element(self.start_ms),
element(self.end_ms),
element(self.size_extension_multiplier().unwrap_or(0)),
element(self.quote_extension_multiplier()),
element(self.settlement_cap),
element(self.settlement.code()),
element(self.default_price),
element(self.taker_fee),
element(self.maker_fee),
element(self.min_base_amount),
element(self.min_quote_amount),
element(self.order_quote_limit),
element(self.open_interest_limit),
element(u8::from(self.frozen)),
]
}
fn info_fields(&self) -> Vec<(&'static str, Json)> {
vec![
("MarketIndex", number(self.slot.get())),
("MarketType", number(MarketKind::BinaryOptions.code())),
("BaseAssetId", number(collateral_id())),
("QuoteAssetId", number(collateral_id())),
("StartTimestamp", number(self.start_ms)),
("EndTimestamp", number(self.end_ms)),
("SizeExtensionMultiplier", number(self.size_extension_multiplier().unwrap_or(0))),
("QuoteExtensionMultiplier", number(self.quote_extension_multiplier())),
("SettlementCap", number(self.settlement_cap)),
("SettlementType", number(self.settlement.code())),
("DefaultPrice", number(self.default_price)),
("TakerFee", number(self.taker_fee)),
("MakerFee", number(self.maker_fee)),
("MinBaseAmount", number(self.min_base_amount)),
("MinQuoteAmount", number(self.min_quote_amount)),
("OrderQuoteLimit", number(self.order_quote_limit)),
("OpenInterestLimit", number(self.open_interest_limit)),
("IsFrozen", number(u8::from(self.frozen))),
("Symbol", byte_list(&self.symbol_bytes())),
("SizeDecimals", number(self.size_decimals)),
("PriceDecimals", number(self.price_decimals)),
]
}
fn check(&self) -> Result<(), BadTx> {
if self.symbol.len() >= SYMBOL_BYTES {
return Err(BadTx::new("Symbol", "a symbol is under 32 bytes"));
}
binary_options_slot(self.slot)?;
check_window(self.start_ms, self.end_ms)?;
let size = check_multiplier(self.size_extension_multiplier(), "SizeExtensionMultiplier")?;
let quote = check_multiplier(Some(self.quote_extension_multiplier()), "QuoteExtensionMultiplier")?;
if self.settlement_cap == 0 || size % quote != 0 || size / quote != u64::from(self.settlement_cap) {
return Err(BadTx::new("SettlementCap", "the payout cap is above 0, the size multiplier over the quote's"));
}
if self.default_price > self.settlement_cap {
return Err(BadTx::new("DefaultPrice", "a refund settles at the payout cap at most"));
}
check_fees(self.taker_fee, self.maker_fee)?;
check_limits(
self.min_base_amount,
self.min_quote_amount,
self.order_quote_limit,
self.open_interest_limit,
MarketKind::BinaryOptions,
)
}
}
#[cfg(test)]
#[path = "tests/create.rs"]
mod tests;