further work on perps
Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
34d14ef267
commit
ec5e959804
|
@ -448,9 +448,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.7.3"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f"
|
||||
checksum = "0e851ca7c24871e7336801608a4797d7376545b6928a10d32d75685687141ead"
|
||||
dependencies = [
|
||||
"bytemuck_derive",
|
||||
]
|
||||
|
@ -1542,6 +1542,15 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mango-macro"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mango-v4"
|
||||
version = "0.1.0"
|
||||
|
@ -1558,6 +1567,7 @@ dependencies = [
|
|||
"fixed",
|
||||
"fixed-macro",
|
||||
"log",
|
||||
"mango-macro",
|
||||
"margin-trade",
|
||||
"num_enum",
|
||||
"pyth-client",
|
||||
|
@ -2012,9 +2022,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.15"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
|
||||
checksum = "b4af2ec4714533fcdf07e886f17025ace8b997b9ce51204ee69b6da831c3da57"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
@ -3056,9 +3066,9 @@ checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.86"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
|
||||
checksum = "ea297be220d52398dcc07ce15a209fce436d361735ac1db700cab3b6cdfb9f54"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "mango-macro"
|
||||
version = "0.0.1"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = "1.0.89"
|
||||
bytemuck = "1.8.0"
|
||||
quote = "1.0.16"
|
|
@ -0,0 +1,20 @@
|
|||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
#[proc_macro_derive(Pod)]
|
||||
pub fn pod(input: TokenStream) -> TokenStream {
|
||||
let DeriveInput { ident, data, .. } = parse_macro_input!(input);
|
||||
|
||||
match data {
|
||||
syn::Data::Struct(_) => {
|
||||
quote! {
|
||||
unsafe impl bytemuck::Zeroable for #ident {}
|
||||
unsafe impl bytemuck::Pod for #ident {}
|
||||
}
|
||||
}
|
||||
|
||||
_ => panic!(),
|
||||
}
|
||||
.into()
|
||||
}
|
|
@ -35,6 +35,7 @@ checked_math = { path = "../../lib/checked_math" }
|
|||
arrayref = "0.3.6"
|
||||
num_enum = "0.5.1"
|
||||
bincode = "1.3.3"
|
||||
mango-macro={ path = "../../mango-macro" }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-sdk = { version = "1.9.5", default-features = false }
|
||||
|
|
|
@ -22,30 +22,13 @@ pub struct CreatePerpMarket<'info> {
|
|||
space = 8 + std::mem::size_of::<PerpMarket>(),
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
#[account(
|
||||
init,
|
||||
seeds = [group.key().as_ref(), b"Asks".as_ref(), perp_market.key().as_ref()],
|
||||
bump,
|
||||
payer = payer,
|
||||
space = 8 + std::mem::size_of::<Book>(),
|
||||
)]
|
||||
pub asks: AccountLoader<'info, crate::state::Book>,
|
||||
#[account(
|
||||
init,
|
||||
seeds = [group.key().as_ref(), b"Bids".as_ref(), perp_market.key().as_ref()],
|
||||
bump,
|
||||
payer = payer,
|
||||
space = 8 + std::mem::size_of::<Book>(),
|
||||
)]
|
||||
pub bids: AccountLoader<'info, Book>,
|
||||
#[account(
|
||||
init,
|
||||
seeds = [group.key().as_ref(), b"EventQueue".as_ref(), perp_market.key().as_ref()],
|
||||
bump,
|
||||
payer = payer,
|
||||
space = 8 + std::mem::size_of::<EventQueue>(),
|
||||
)]
|
||||
pub event_queue: AccountLoader<'info, crate::state::EventQueue>,
|
||||
|
||||
/// Accounts are initialised by client,
|
||||
/// anchor discriminator is set first when ix exits,
|
||||
#[account(zero)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
#[account(zero)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
|
||||
#[account(mut)]
|
||||
pub payer: Signer<'info>,
|
||||
|
@ -60,11 +43,6 @@ pub fn create_perp_market(
|
|||
quote_token_index: TokenIndex,
|
||||
quote_lot_size: i64,
|
||||
base_lot_size: i64,
|
||||
// todo
|
||||
// base token index (optional)
|
||||
// quote token index
|
||||
// oracle
|
||||
// perp market index
|
||||
) -> Result<()> {
|
||||
let mut perp_market = ctx.accounts.perp_market.load_init()?;
|
||||
*perp_market = PerpMarket {
|
||||
|
@ -72,31 +50,20 @@ pub fn create_perp_market(
|
|||
oracle: ctx.accounts.oracle.key(),
|
||||
bids: ctx.accounts.bids.key(),
|
||||
asks: ctx.accounts.asks.key(),
|
||||
event_queue: ctx.accounts.event_queue.key(),
|
||||
quote_lot_size: quote_lot_size,
|
||||
base_lot_size: base_lot_size,
|
||||
// long_funding,
|
||||
// short_funding,
|
||||
// last_updated,
|
||||
// open_interest,
|
||||
seq_num: 0,
|
||||
// fees_accrued,
|
||||
// liquidity_mining_info,
|
||||
// mngo_vault: ctx.accounts.mngo_vault.key(),
|
||||
bump: *ctx.bumps.get("perp_market").ok_or(MangoError::SomeError)?,
|
||||
perp_market_index,
|
||||
base_token_index: base_token_index_opt.ok_or(TokenIndex::MAX).unwrap(),
|
||||
quote_token_index,
|
||||
bump: *ctx.bumps.get("perp_market").ok_or(MangoError::SomeError)?,
|
||||
};
|
||||
|
||||
let mut asks = ctx.accounts.asks.load_init()?;
|
||||
*asks = Book {};
|
||||
|
||||
let mut bids = ctx.accounts.bids.load_init()?;
|
||||
*bids = Book {};
|
||||
bids.book_side_type = BookSideType::Bids;
|
||||
|
||||
let mut event_queue = ctx.accounts.event_queue.load_init()?;
|
||||
*event_queue = EventQueue {};
|
||||
let mut asks = ctx.accounts.asks.load_init()?;
|
||||
asks.book_side_type = BookSideType::Asks;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,8 +1,100 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct PlacePerpOrder {}
|
||||
use crate::state::{
|
||||
oracle_price, Book, BookSide, Group, MangoAccount, OrderType, PerpMarket, Side,
|
||||
};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct PlacePerpOrder<'info> {
|
||||
pub group: AccountLoader<'info, Group>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = owner,
|
||||
)]
|
||||
pub account: AccountLoader<'info, MangoAccount>,
|
||||
|
||||
#[account(
|
||||
mut,
|
||||
has_one = group,
|
||||
has_one = bids,
|
||||
has_one = asks,
|
||||
has_one = oracle,
|
||||
)]
|
||||
pub perp_market: AccountLoader<'info, PerpMarket>,
|
||||
#[account(mut)]
|
||||
pub asks: AccountLoader<'info, BookSide>,
|
||||
#[account(mut)]
|
||||
pub bids: AccountLoader<'info, BookSide>,
|
||||
pub oracle: UncheckedAccount<'info>,
|
||||
|
||||
pub owner: Signer<'info>,
|
||||
}
|
||||
|
||||
pub fn place_perp_order(
|
||||
ctx: Context<PlacePerpOrder>,
|
||||
// TODO side is harcoded for now
|
||||
// maybe new_bid and new_ask can be folded into one function
|
||||
// side: Side,
|
||||
price: i64,
|
||||
max_base_quantity: i64,
|
||||
max_quote_quantity: i64,
|
||||
client_order_id: u64,
|
||||
order_type: OrderType,
|
||||
// TODO reduce_only relies on event queue
|
||||
// reduce_only: bool,
|
||||
expiry_timestamp: u64,
|
||||
limit: u8,
|
||||
) -> Result<()> {
|
||||
let mut account = ctx.accounts.account.load_mut()?;
|
||||
let mango_account_pk = ctx.accounts.account.key();
|
||||
|
||||
let mut perp_market = ctx.accounts.perp_market.load_mut()?;
|
||||
let bids = &ctx.accounts.bids.to_account_info();
|
||||
let asks = &ctx.accounts.asks.to_account_info();
|
||||
let mut book = Book::load_checked(&bids, &asks, &perp_market)?;
|
||||
|
||||
let oracle_price = oracle_price(&ctx.accounts.oracle.to_account_info())?;
|
||||
|
||||
let now_ts = Clock::get()?.unix_timestamp as u64;
|
||||
let time_in_force = if expiry_timestamp != 0 {
|
||||
// If expiry is far in the future, clamp to 255 seconds
|
||||
let tif = expiry_timestamp.saturating_sub(now_ts).min(255);
|
||||
if tif == 0 {
|
||||
// If expiry is in the past, ignore the order
|
||||
msg!("Order is already expired");
|
||||
return Ok(());
|
||||
}
|
||||
tif as u8
|
||||
} else {
|
||||
// Never expire
|
||||
0
|
||||
};
|
||||
|
||||
// TODO reduce_only based on event queue
|
||||
|
||||
book.new_bid(
|
||||
// program_id: &Pubkey,
|
||||
// mango_group: &MangoGroup,
|
||||
// mango_group_pk: &Pubkey,
|
||||
// mango_cache: &MangoCache,
|
||||
// event_queue: &mut EventQueue,
|
||||
&mut perp_market,
|
||||
oracle_price,
|
||||
&mut account,
|
||||
&mango_account_pk,
|
||||
// market_index: usize,
|
||||
price,
|
||||
max_base_quantity,
|
||||
max_quote_quantity,
|
||||
order_type,
|
||||
time_in_force,
|
||||
client_order_id,
|
||||
now_ts,
|
||||
// referrer_mango_account_ai: Option<&AccountInfo>,
|
||||
limit,
|
||||
)?;
|
||||
|
||||
pub fn place_perp_order(_ctx: Context<PlacePerpOrder>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ pub mod instructions;
|
|||
mod serum3_cpi;
|
||||
pub mod state;
|
||||
|
||||
use state::{PerpMarketIndex, Serum3MarketIndex, TokenIndex};
|
||||
use state::{OrderType, PerpMarketIndex, Serum3MarketIndex, Side, TokenIndex};
|
||||
|
||||
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||
|
||||
|
@ -143,8 +143,30 @@ pub mod mango_v4 {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn place_perp_order(ctx: Context<PlacePerpOrder>) -> Result<()> {
|
||||
instructions::place_perp_order(ctx)
|
||||
pub fn place_perp_order(
|
||||
ctx: Context<PlacePerpOrder>,
|
||||
side: Side,
|
||||
price: i64,
|
||||
max_base_quantity: i64,
|
||||
max_quote_quantity: i64,
|
||||
client_order_id: u64,
|
||||
order_type: OrderType,
|
||||
reduce_only: bool,
|
||||
expiry_timestamp: u64,
|
||||
limit: u8,
|
||||
) -> Result<()> {
|
||||
instructions::place_perp_order(
|
||||
ctx,
|
||||
// side,
|
||||
price,
|
||||
max_base_quantity,
|
||||
max_quote_quantity,
|
||||
client_order_id,
|
||||
order_type,
|
||||
// reduce_only,
|
||||
// expiry_timestamp,
|
||||
limit,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ use crate::state::*;
|
|||
// MangoAccount size and health compute needs.
|
||||
const MAX_INDEXED_POSITIONS: usize = 16;
|
||||
const MAX_SERUM_OPEN_ORDERS: usize = 8;
|
||||
const MAX_PERP_OPEN_ORDERS: usize = 8;
|
||||
|
||||
#[zero_copy]
|
||||
pub struct TokenAccount {
|
||||
|
|
|
@ -4,6 +4,7 @@ pub use health::*;
|
|||
pub use mango_account::*;
|
||||
pub use mint_info::*;
|
||||
pub use oracle::*;
|
||||
pub use orderbook::*;
|
||||
pub use perp_market::*;
|
||||
pub use serum3_market::*;
|
||||
|
||||
|
@ -13,5 +14,6 @@ mod health;
|
|||
mod mango_account;
|
||||
mod mint_info;
|
||||
mod oracle;
|
||||
pub mod orderbook;
|
||||
mod perp_market;
|
||||
mod serum3_market;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,468 @@
|
|||
use bytemuck::Zeroable;
|
||||
use std::cell::RefMut;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use bytemuck::{cast, cast_mut, cast_ref};
|
||||
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
|
||||
use crate::state::orderbook::bookside_iterator::BookSideIter;
|
||||
use crate::state::PerpMarket;
|
||||
|
||||
use crate::error::MangoError;
|
||||
use crate::state::orderbook::nodes::{
|
||||
AnyNode, FreeNode, InnerNode, LeafNode, NodeHandle, NodeRef, NodeTag,
|
||||
};
|
||||
use crate::util::LoadZeroCopy;
|
||||
|
||||
use super::Book;
|
||||
|
||||
pub const MAX_BOOK_NODES: usize = 1024;
|
||||
|
||||
#[derive(
|
||||
Eq,
|
||||
PartialEq,
|
||||
Copy,
|
||||
Clone,
|
||||
TryFromPrimitive,
|
||||
IntoPrimitive,
|
||||
Debug,
|
||||
AnchorSerialize,
|
||||
AnchorDeserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum BookSideType {
|
||||
Bids,
|
||||
Asks,
|
||||
}
|
||||
|
||||
/// A binary tree on AnyNode::key()
|
||||
///
|
||||
/// The key encodes the price in the top 64 bits.
|
||||
#[account(zero_copy)]
|
||||
pub struct BookSide {
|
||||
// pub meta_data: MetaData,
|
||||
// todo: do we want this type at this level?
|
||||
pub book_side_type: BookSideType,
|
||||
pub bump_index: usize,
|
||||
pub free_list_len: usize,
|
||||
pub free_list_head: NodeHandle,
|
||||
pub root_node: NodeHandle,
|
||||
pub leaf_count: usize,
|
||||
pub nodes: [AnyNode; MAX_BOOK_NODES],
|
||||
}
|
||||
|
||||
impl BookSide {
|
||||
/// Iterate over all entries in the book filtering out invalid orders
|
||||
///
|
||||
/// smallest to highest for asks
|
||||
/// highest to smallest for bids
|
||||
pub fn iter_valid(&self, now_ts: u64) -> BookSideIter {
|
||||
BookSideIter::new(self, now_ts)
|
||||
}
|
||||
|
||||
/// Iterate over all entries, including invalid orders
|
||||
pub fn iter_all_including_invalid(&self) -> BookSideIter {
|
||||
BookSideIter::new(self, 0)
|
||||
}
|
||||
|
||||
pub fn load_mut_checked<'a>(
|
||||
account: &'a AccountInfo,
|
||||
perp_market: &PerpMarket,
|
||||
) -> Result<RefMut<'a, Self>> {
|
||||
let state = account.load_mut::<BookSide>()?;
|
||||
|
||||
match state.book_side_type {
|
||||
BookSideType::Bids => require!(account.key == &perp_market.bids, MangoError::SomeError),
|
||||
BookSideType::Asks => require!(account.key == &perp_market.asks, MangoError::SomeError),
|
||||
}
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
//
|
||||
// pub fn load_and_init<'a>(
|
||||
// account: &'a AccountInfo,
|
||||
// program_id: &Pubkey,
|
||||
// data_type: DataType,
|
||||
// rent: &Rent,
|
||||
// ) -> MangoResult<RefMut<'a, Self>> {
|
||||
// // NOTE: require this first so we can borrow account later
|
||||
// require!(
|
||||
// rent.is_exempt(account.lamports(), account.data_len()),
|
||||
// MangoErrorCode::AccountNotRentExempt
|
||||
// )?;
|
||||
//
|
||||
// let mut state = Self::load_mut(account)?;
|
||||
// require!(account.owner == program_id, MangoError::SomeError)?; // todo invalid owner
|
||||
// require!(!state.meta_data.is_initialized, MangoError::SomeError)?; // todo
|
||||
// state.meta_data = MetaData::new(data_type, 0, true);
|
||||
// Ok(state)
|
||||
// }
|
||||
|
||||
pub fn get_mut(&mut self, key: NodeHandle) -> Option<&mut AnyNode> {
|
||||
let node = &mut self.nodes[key as usize];
|
||||
let tag = NodeTag::try_from(node.tag);
|
||||
match tag {
|
||||
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => Some(node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn get(&self, key: NodeHandle) -> Option<&AnyNode> {
|
||||
let node = &self.nodes[key as usize];
|
||||
let tag = NodeTag::try_from(node.tag);
|
||||
match tag {
|
||||
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => Some(node),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_min(&mut self) -> Option<LeafNode> {
|
||||
self.remove_by_key(self.get(self.find_min()?)?.key()?)
|
||||
}
|
||||
|
||||
pub fn remove_max(&mut self) -> Option<LeafNode> {
|
||||
self.remove_by_key(self.get(self.find_max()?)?.key()?)
|
||||
}
|
||||
|
||||
/// Remove the order with the lowest expiry timestamp, if that's < now_ts.
|
||||
pub fn remove_one_expired(&mut self, now_ts: u64) -> Option<LeafNode> {
|
||||
let (expired_h, expires_at) = self.find_earliest_expiry()?;
|
||||
if expires_at < now_ts {
|
||||
self.remove_by_key(self.get(expired_h)?.key()?)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_max(&self) -> Option<NodeHandle> {
|
||||
self.find_min_max(true)
|
||||
}
|
||||
|
||||
pub fn root(&self) -> Option<NodeHandle> {
|
||||
if self.leaf_count == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(self.root_node)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_min(&self) -> Option<NodeHandle> {
|
||||
self.find_min_max(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn to_price_quantity_vec(&self, reverse: bool) -> Vec<(i64, i64)> {
|
||||
let mut pqs = vec![];
|
||||
let mut current: NodeHandle = match self.root() {
|
||||
None => return pqs,
|
||||
Some(node_handle) => node_handle,
|
||||
};
|
||||
|
||||
let left = reverse as usize;
|
||||
let right = !reverse as usize;
|
||||
let mut stack = vec![];
|
||||
loop {
|
||||
let root_contents = self.get(current).unwrap(); // should never fail unless book is already fucked
|
||||
match root_contents.case().unwrap() {
|
||||
NodeRef::Inner(inner) => {
|
||||
stack.push(inner);
|
||||
current = inner.children[left];
|
||||
}
|
||||
NodeRef::Leaf(leaf) => {
|
||||
// if you hit leaf then pop stack and go right
|
||||
// all inner nodes on stack have already been visited to the left
|
||||
pqs.push((leaf.price(), leaf.quantity));
|
||||
match stack.pop() {
|
||||
None => return pqs,
|
||||
Some(inner) => {
|
||||
current = inner.children[right];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_min_max(&self, find_max: bool) -> Option<NodeHandle> {
|
||||
let mut root: NodeHandle = self.root()?;
|
||||
|
||||
let i = if find_max { 1 } else { 0 };
|
||||
loop {
|
||||
let root_contents = self.get(root).unwrap();
|
||||
match root_contents.case().unwrap() {
|
||||
NodeRef::Inner(&InnerNode { children, .. }) => {
|
||||
root = children[i];
|
||||
}
|
||||
_ => return Some(root),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_min(&self) -> Option<&LeafNode> {
|
||||
self.get_min_max(false)
|
||||
}
|
||||
|
||||
pub fn get_max(&self) -> Option<&LeafNode> {
|
||||
self.get_min_max(true)
|
||||
}
|
||||
pub fn get_min_max(&self, find_max: bool) -> Option<&LeafNode> {
|
||||
let mut root: NodeHandle = self.root()?;
|
||||
|
||||
let i = if find_max { 1 } else { 0 };
|
||||
loop {
|
||||
let root_contents = self.get(root)?;
|
||||
match root_contents.case()? {
|
||||
NodeRef::Inner(inner) => {
|
||||
root = inner.children[i];
|
||||
}
|
||||
NodeRef::Leaf(leaf) => {
|
||||
return Some(leaf);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_by_key(&mut self, search_key: i128) -> Option<LeafNode> {
|
||||
// path of InnerNode handles that lead to the removed leaf
|
||||
let mut stack: Vec<(NodeHandle, bool)> = vec![];
|
||||
|
||||
// special case potentially removing the root
|
||||
let mut parent_h = self.root()?;
|
||||
let (mut child_h, mut crit_bit) = match self.get(parent_h).unwrap().case().unwrap() {
|
||||
NodeRef::Leaf(&leaf) if leaf.key == search_key => {
|
||||
assert_eq!(self.leaf_count, 1);
|
||||
self.root_node = 0;
|
||||
self.leaf_count = 0;
|
||||
let _old_root = self.remove(parent_h).unwrap();
|
||||
return Some(leaf);
|
||||
}
|
||||
NodeRef::Leaf(_) => return None,
|
||||
NodeRef::Inner(inner) => inner.walk_down(search_key),
|
||||
};
|
||||
stack.push((parent_h, crit_bit));
|
||||
|
||||
// walk down the tree until finding the key
|
||||
loop {
|
||||
match self.get(child_h).unwrap().case().unwrap() {
|
||||
NodeRef::Inner(inner) => {
|
||||
parent_h = child_h;
|
||||
let (new_child_h, new_crit_bit) = inner.walk_down(search_key);
|
||||
child_h = new_child_h;
|
||||
crit_bit = new_crit_bit;
|
||||
stack.push((parent_h, crit_bit));
|
||||
}
|
||||
NodeRef::Leaf(leaf) => {
|
||||
if leaf.key != search_key {
|
||||
return None;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// replace parent with its remaining child node
|
||||
// free child_h, replace *parent_h with *other_child_h, free other_child_h
|
||||
let other_child_h = self.get(parent_h).unwrap().children().unwrap()[!crit_bit as usize];
|
||||
let other_child_node_contents = self.remove(other_child_h).unwrap();
|
||||
let new_expiry = other_child_node_contents.earliest_expiry();
|
||||
*self.get_mut(parent_h).unwrap() = other_child_node_contents;
|
||||
self.leaf_count -= 1;
|
||||
let removed_leaf: LeafNode = cast(self.remove(child_h).unwrap());
|
||||
|
||||
// update child min expiry back up to the root
|
||||
let outdated_expiry = removed_leaf.expiry();
|
||||
stack.pop(); // the final parent has been replaced by the remaining leaf
|
||||
self.update_parent_earliest_expiry(&stack, outdated_expiry, new_expiry);
|
||||
|
||||
Some(removed_leaf)
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: NodeHandle) -> Option<AnyNode> {
|
||||
let val = *self.get(key)?;
|
||||
|
||||
self.nodes[key as usize] = cast(FreeNode {
|
||||
tag: if self.free_list_len == 0 {
|
||||
NodeTag::LastFreeNode.into()
|
||||
} else {
|
||||
NodeTag::FreeNode.into()
|
||||
},
|
||||
next: self.free_list_head,
|
||||
reserve: [0; 80],
|
||||
});
|
||||
|
||||
self.free_list_len += 1;
|
||||
self.free_list_head = key;
|
||||
Some(val)
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, val: &AnyNode) -> Result<NodeHandle> {
|
||||
match NodeTag::try_from(val.tag) {
|
||||
Ok(NodeTag::InnerNode) | Ok(NodeTag::LeafNode) => (),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if self.free_list_len == 0 {
|
||||
require!(
|
||||
self.bump_index < self.nodes.len() && self.bump_index < (u32::MAX as usize),
|
||||
MangoError::SomeError // todo
|
||||
);
|
||||
|
||||
self.nodes[self.bump_index] = *val;
|
||||
let key = self.bump_index as u32;
|
||||
self.bump_index += 1;
|
||||
return Ok(key);
|
||||
}
|
||||
|
||||
let key = self.free_list_head;
|
||||
let node = &mut self.nodes[key as usize];
|
||||
|
||||
// TODO OPT possibly unnecessary require here - remove if we need compute
|
||||
match NodeTag::try_from(node.tag) {
|
||||
Ok(NodeTag::FreeNode) => assert!(self.free_list_len > 1),
|
||||
Ok(NodeTag::LastFreeNode) => assert_eq!(self.free_list_len, 1),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
// TODO - test borrow requireer
|
||||
self.free_list_head = cast_ref::<AnyNode, FreeNode>(node).next;
|
||||
self.free_list_len -= 1;
|
||||
*node = *val;
|
||||
Ok(key)
|
||||
}
|
||||
pub fn insert_leaf(&mut self, new_leaf: &LeafNode) -> Result<(NodeHandle, Option<LeafNode>)> {
|
||||
// path of InnerNode handles that lead to the new leaf
|
||||
let mut stack: Vec<(NodeHandle, bool)> = vec![];
|
||||
|
||||
// deal with inserts into an empty tree
|
||||
let mut root: NodeHandle = match self.root() {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
// create a new root if none exists
|
||||
let handle = self.insert(new_leaf.as_ref())?;
|
||||
self.root_node = handle;
|
||||
self.leaf_count = 1;
|
||||
return Ok((handle, None));
|
||||
}
|
||||
};
|
||||
|
||||
// walk down the tree until we find the insert location
|
||||
loop {
|
||||
// require if the new node will be a child of the root
|
||||
let root_contents = *self.get(root).unwrap();
|
||||
let root_key = root_contents.key().unwrap();
|
||||
if root_key == new_leaf.key {
|
||||
// This should never happen because key should never match
|
||||
if let Some(NodeRef::Leaf(&old_root_as_leaf)) = root_contents.case() {
|
||||
// clobber the existing leaf
|
||||
*self.get_mut(root).unwrap() = *new_leaf.as_ref();
|
||||
self.update_parent_earliest_expiry(
|
||||
&stack,
|
||||
old_root_as_leaf.expiry(),
|
||||
new_leaf.expiry(),
|
||||
);
|
||||
return Ok((root, Some(old_root_as_leaf)));
|
||||
}
|
||||
// InnerNodes have a random child's key, so matching can happen and is fine
|
||||
}
|
||||
let shared_prefix_len: u32 = (root_key ^ new_leaf.key).leading_zeros();
|
||||
match root_contents.case() {
|
||||
None => unreachable!(),
|
||||
Some(NodeRef::Inner(inner)) => {
|
||||
let keep_old_root = shared_prefix_len >= inner.prefix_len;
|
||||
if keep_old_root {
|
||||
let (child, crit_bit) = inner.walk_down(new_leaf.key);
|
||||
stack.push((root, crit_bit));
|
||||
root = child;
|
||||
continue;
|
||||
};
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
// implies root is a Leaf or Inner where shared_prefix_len < prefix_len
|
||||
// we'll replace root with a new InnerNode that has new_leaf and root as children
|
||||
|
||||
// change the root in place to represent the LCA of [new_leaf] and [root]
|
||||
let crit_bit_mask: i128 = 1i128 << (127 - shared_prefix_len);
|
||||
let new_leaf_crit_bit = (crit_bit_mask & new_leaf.key) != 0;
|
||||
let old_root_crit_bit = !new_leaf_crit_bit;
|
||||
|
||||
let new_leaf_handle = self.insert(new_leaf.as_ref())?;
|
||||
let moved_root_handle = match self.insert(&root_contents) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
self.remove(new_leaf_handle).unwrap();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
let new_root: &mut InnerNode = cast_mut(self.get_mut(root).unwrap());
|
||||
*new_root = InnerNode::new(shared_prefix_len, new_leaf.key);
|
||||
|
||||
new_root.children[new_leaf_crit_bit as usize] = new_leaf_handle;
|
||||
new_root.children[old_root_crit_bit as usize] = moved_root_handle;
|
||||
|
||||
let new_leaf_expiry = new_leaf.expiry();
|
||||
let old_root_expiry = root_contents.earliest_expiry();
|
||||
new_root.child_earliest_expiry[new_leaf_crit_bit as usize] = new_leaf_expiry;
|
||||
new_root.child_earliest_expiry[old_root_crit_bit as usize] = old_root_expiry;
|
||||
|
||||
// walk up the stack and fix up the new min if needed
|
||||
if new_leaf_expiry < old_root_expiry {
|
||||
self.update_parent_earliest_expiry(&stack, old_root_expiry, new_leaf_expiry);
|
||||
}
|
||||
|
||||
self.leaf_count += 1;
|
||||
return Ok((new_leaf_handle, None));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_full(&self) -> bool {
|
||||
self.free_list_len <= 1 && self.bump_index >= self.nodes.len() - 1
|
||||
}
|
||||
|
||||
/// When a node changes, the parents' child_earliest_expiry may need to be updated.
|
||||
///
|
||||
/// This function walks up the `stack` of parents and applies the change where the
|
||||
/// previous child's `outdated_expiry` is replaced by `new_expiry`.
|
||||
pub fn update_parent_earliest_expiry(
|
||||
&mut self,
|
||||
stack: &[(NodeHandle, bool)],
|
||||
mut outdated_expiry: u64,
|
||||
mut new_expiry: u64,
|
||||
) {
|
||||
// Walk from the top of the stack to the root of the tree.
|
||||
// Since the stack grows by appending, we need to iterate the slice in reverse order.
|
||||
for (parent_h, crit_bit) in stack.iter().rev() {
|
||||
let parent = self.get_mut(*parent_h).unwrap().as_inner_mut().unwrap();
|
||||
if parent.child_earliest_expiry[*crit_bit as usize] != outdated_expiry {
|
||||
break;
|
||||
}
|
||||
outdated_expiry = parent.earliest_expiry();
|
||||
parent.child_earliest_expiry[*crit_bit as usize] = new_expiry;
|
||||
new_expiry = parent.earliest_expiry();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the handle of the node with the lowest expiry timestamp, and this timestamp
|
||||
pub fn find_earliest_expiry(&self) -> Option<(NodeHandle, u64)> {
|
||||
let mut current: NodeHandle = match self.root() {
|
||||
Some(h) => h,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
loop {
|
||||
let contents = *self.get(current).unwrap();
|
||||
match contents.case() {
|
||||
None => unreachable!(),
|
||||
Some(NodeRef::Inner(inner)) => {
|
||||
current = inner.children[(inner.child_earliest_expiry[0]
|
||||
> inner.child_earliest_expiry[1])
|
||||
as usize];
|
||||
}
|
||||
_ => {
|
||||
return Some((current, contents.earliest_expiry()));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
use crate::state::orderbook::bookside::{BookSide, BookSideType};
|
||||
use crate::state::orderbook::nodes::{InnerNode, LeafNode, NodeHandle, NodeRef};
|
||||
|
||||
/// Iterate over orders in order (bids=descending, asks=ascending)
|
||||
pub struct BookSideIter<'a> {
|
||||
book_side: &'a BookSide,
|
||||
/// InnerNodes where the right side still needs to be iterated on
|
||||
stack: Vec<&'a InnerNode>,
|
||||
/// To be returned on `next()`
|
||||
next_leaf: Option<(NodeHandle, &'a LeafNode)>,
|
||||
|
||||
/// either 0, 1 to iterate low-to-high, or 1, 0 to iterate high-to-low
|
||||
left: usize,
|
||||
right: usize,
|
||||
|
||||
now_ts: u64,
|
||||
}
|
||||
|
||||
impl<'a> BookSideIter<'a> {
|
||||
pub fn new(book_side: &'a BookSide, now_ts: u64) -> Self {
|
||||
let (left, right) = if book_side.book_side_type == BookSideType::Bids {
|
||||
(1, 0)
|
||||
} else {
|
||||
(0, 1)
|
||||
};
|
||||
let stack = vec![];
|
||||
|
||||
let mut iter = Self {
|
||||
book_side,
|
||||
stack,
|
||||
next_leaf: None,
|
||||
left,
|
||||
right,
|
||||
now_ts,
|
||||
};
|
||||
if book_side.leaf_count != 0 {
|
||||
iter.next_leaf = iter.find_leftmost_valid_leaf(book_side.root_node);
|
||||
}
|
||||
iter
|
||||
}
|
||||
|
||||
fn find_leftmost_valid_leaf(
|
||||
&mut self,
|
||||
start: NodeHandle,
|
||||
) -> Option<(NodeHandle, &'a LeafNode)> {
|
||||
let mut current = start;
|
||||
loop {
|
||||
match self.book_side.get(current).unwrap().case().unwrap() {
|
||||
NodeRef::Inner(inner) => {
|
||||
self.stack.push(inner);
|
||||
current = inner.children[self.left];
|
||||
}
|
||||
NodeRef::Leaf(leaf) => {
|
||||
if leaf.is_valid(self.now_ts) {
|
||||
return Some((current, leaf));
|
||||
} else {
|
||||
match self.stack.pop() {
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
Some(inner) => {
|
||||
current = inner.children[self.right];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BookSideIter<'a> {
|
||||
type Item = (NodeHandle, &'a LeafNode);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// if next leaf is None just return it
|
||||
if self.next_leaf.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// start popping from stack and get the other child
|
||||
let current_leaf = self.next_leaf;
|
||||
self.next_leaf = match self.stack.pop() {
|
||||
None => None,
|
||||
Some(inner) => {
|
||||
let start = inner.children[self.right];
|
||||
// go down the left branch as much as possible until reaching a valid leaf
|
||||
self.find_leftmost_valid_leaf(start)
|
||||
}
|
||||
};
|
||||
|
||||
current_leaf
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(IntoPrimitive, TryFromPrimitive)]
|
||||
pub enum DataType {
|
||||
MangoGroup = 0,
|
||||
MangoAccount,
|
||||
RootBank,
|
||||
NodeBank,
|
||||
PerpMarket,
|
||||
Bids,
|
||||
Asks,
|
||||
MangoCache,
|
||||
EventQueue,
|
||||
AdvancedOrders,
|
||||
ReferrerMemory,
|
||||
ReferrerIdRecord,
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
use mango_macro::Pod;
|
||||
|
||||
use super::datatype::DataType;
|
||||
|
||||
#[derive(Copy, Clone, Pod, Default)]
|
||||
#[repr(C)]
|
||||
/// Stores meta information about the `Account` on chain
|
||||
pub struct MetaData {
|
||||
// pub data_type: u8,
|
||||
pub version: u8,
|
||||
// pub is_initialized: bool,
|
||||
// being used by PerpMarket to store liquidity mining param
|
||||
pub extra_info: [u8; 7],
|
||||
}
|
||||
|
||||
impl MetaData {
|
||||
pub fn new(
|
||||
// data_type: DataType,
|
||||
version: u8,
|
||||
// is_initialized: bool
|
||||
) -> Self {
|
||||
Self {
|
||||
// data_type: data_type as u8,
|
||||
version,
|
||||
// is_initialized,
|
||||
extra_info: [0; 7],
|
||||
}
|
||||
}
|
||||
pub fn new_with_extra(
|
||||
// data_type: DataType,
|
||||
version: u8,
|
||||
// is_initialized: bool,
|
||||
extra_info: [u8; 7],
|
||||
) -> Self {
|
||||
Self {
|
||||
// data_type: data_type as u8,
|
||||
version,
|
||||
// is_initialized,
|
||||
extra_info,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
pub use book::*;
|
||||
pub use bookside::*;
|
||||
pub use bookside_iterator::*;
|
||||
pub use datatype::*;
|
||||
pub use metadata::*;
|
||||
pub use nodes::*;
|
||||
pub use ob_utils::*;
|
||||
pub use order_type::*;
|
||||
pub use order_type::*;
|
||||
pub use queue::*;
|
||||
|
||||
pub mod book;
|
||||
pub mod bookside;
|
||||
pub mod bookside_iterator;
|
||||
pub mod datatype;
|
||||
pub mod metadata;
|
||||
pub mod nodes;
|
||||
pub mod ob_utils;
|
||||
pub mod order_type;
|
||||
pub mod queue;
|
|
@ -0,0 +1,272 @@
|
|||
use std::mem::size_of;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use bytemuck::{cast_mut, cast_ref};
|
||||
use mango_macro::Pod;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use static_assertions::const_assert_eq;
|
||||
|
||||
use super::order_type::OrderType;
|
||||
|
||||
pub type NodeHandle = u32;
|
||||
const NODE_SIZE: usize = 88;
|
||||
|
||||
#[derive(IntoPrimitive, TryFromPrimitive)]
|
||||
#[repr(u32)]
|
||||
pub enum NodeTag {
|
||||
Uninitialized = 0,
|
||||
InnerNode = 1,
|
||||
LeafNode = 2,
|
||||
FreeNode = 3,
|
||||
LastFreeNode = 4,
|
||||
}
|
||||
|
||||
/// InnerNodes and LeafNodes compose the binary tree of orders.
|
||||
///
|
||||
/// Each InnerNode has exactly two children, which are either InnerNodes themselves,
|
||||
/// or LeafNodes. The children share the top `prefix_len` bits of `key`. The left
|
||||
/// child has a 0 in the next bit, and the right a 1.
|
||||
#[derive(Copy, Clone, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct InnerNode {
|
||||
pub tag: u32,
|
||||
/// number of highest `key` bits that all children share
|
||||
/// e.g. if it's 2, the two highest bits of `key` will be the same on all children
|
||||
pub prefix_len: u32,
|
||||
|
||||
/// only the top `prefix_len` bits of `key` are relevant
|
||||
pub key: i128,
|
||||
|
||||
/// indexes into `BookSide::nodes`
|
||||
pub children: [NodeHandle; 2],
|
||||
|
||||
/// The earliest expiry timestamp for the left and right subtrees.
|
||||
///
|
||||
/// Needed to be able to find and remove expired orders without having to
|
||||
/// iterate through the whole bookside.
|
||||
pub child_earliest_expiry: [u64; 2],
|
||||
|
||||
pub reserve: [u8; NODE_SIZE - 48],
|
||||
}
|
||||
|
||||
impl InnerNode {
|
||||
pub fn new(prefix_len: u32, key: i128) -> Self {
|
||||
Self {
|
||||
tag: NodeTag::InnerNode.into(),
|
||||
prefix_len,
|
||||
key,
|
||||
children: [0; 2],
|
||||
child_earliest_expiry: [u64::MAX; 2],
|
||||
reserve: [0; NODE_SIZE - 48],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the handle of the child that may contain the search key
|
||||
/// and 0 or 1 depending on which child it was.
|
||||
pub(crate) fn walk_down(&self, search_key: i128) -> (NodeHandle, bool) {
|
||||
let crit_bit_mask = 1i128 << (127 - self.prefix_len);
|
||||
let crit_bit = (search_key & crit_bit_mask) != 0;
|
||||
(self.children[crit_bit as usize], crit_bit)
|
||||
}
|
||||
|
||||
/// The lowest timestamp at which one of the contained LeafNodes expires.
|
||||
#[inline(always)]
|
||||
pub fn earliest_expiry(&self) -> u64 {
|
||||
std::cmp::min(self.child_earliest_expiry[0], self.child_earliest_expiry[1])
|
||||
}
|
||||
}
|
||||
|
||||
/// LeafNodes represent an order in the binary tree
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct LeafNode {
|
||||
pub tag: u32,
|
||||
pub owner_slot: u8,
|
||||
pub order_type: OrderType, // this was added for TradingView move order
|
||||
pub version: u8,
|
||||
|
||||
/// Time in seconds after `timestamp` at which the order expires.
|
||||
/// A value of 0 means no expiry.
|
||||
pub time_in_force: u8,
|
||||
|
||||
/// The binary tree key
|
||||
pub key: i128,
|
||||
|
||||
pub owner: Pubkey,
|
||||
pub quantity: i64,
|
||||
pub client_order_id: u64,
|
||||
|
||||
// Liquidity incentive related parameters
|
||||
// Either the best bid or best ask at the time the order was placed
|
||||
pub best_initial: i64,
|
||||
|
||||
// The time the order was placed
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn key_to_price(key: i128) -> i64 {
|
||||
(key >> 64) as i64
|
||||
}
|
||||
impl LeafNode {
|
||||
pub fn new(
|
||||
version: u8,
|
||||
owner_slot: u8,
|
||||
key: i128,
|
||||
owner: Pubkey,
|
||||
quantity: i64,
|
||||
client_order_id: u64,
|
||||
timestamp: u64,
|
||||
best_initial: i64,
|
||||
order_type: OrderType,
|
||||
time_in_force: u8,
|
||||
) -> Self {
|
||||
Self {
|
||||
tag: NodeTag::LeafNode.into(),
|
||||
owner_slot,
|
||||
order_type,
|
||||
version,
|
||||
time_in_force,
|
||||
key,
|
||||
owner,
|
||||
quantity,
|
||||
client_order_id,
|
||||
best_initial,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn price(&self) -> i64 {
|
||||
key_to_price(self.key)
|
||||
}
|
||||
|
||||
/// Time at which this order will expire, u64::MAX if never
|
||||
#[inline(always)]
|
||||
pub fn expiry(&self) -> u64 {
|
||||
if self.time_in_force == 0 {
|
||||
u64::MAX
|
||||
} else {
|
||||
self.timestamp + self.time_in_force as u64
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_valid(&self, now_ts: u64) -> bool {
|
||||
self.time_in_force == 0 || now_ts < self.timestamp + self.time_in_force as u64
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct FreeNode {
|
||||
pub(crate) tag: u32,
|
||||
pub(crate) next: NodeHandle,
|
||||
pub(crate) reserve: [u8; NODE_SIZE - 8],
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct AnyNode {
|
||||
pub tag: u32,
|
||||
pub data: [u8; NODE_SIZE - 4],
|
||||
}
|
||||
|
||||
const_assert_eq!(size_of::<AnyNode>(), size_of::<InnerNode>());
|
||||
const_assert_eq!(size_of::<AnyNode>(), size_of::<LeafNode>());
|
||||
const_assert_eq!(size_of::<AnyNode>(), size_of::<FreeNode>());
|
||||
|
||||
pub enum NodeRef<'a> {
|
||||
Inner(&'a InnerNode),
|
||||
Leaf(&'a LeafNode),
|
||||
}
|
||||
|
||||
pub enum NodeRefMut<'a> {
|
||||
Inner(&'a mut InnerNode),
|
||||
Leaf(&'a mut LeafNode),
|
||||
}
|
||||
|
||||
impl AnyNode {
|
||||
pub fn key(&self) -> Option<i128> {
|
||||
match self.case()? {
|
||||
NodeRef::Inner(inner) => Some(inner.key),
|
||||
NodeRef::Leaf(leaf) => Some(leaf.key),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn children(&self) -> Option<[NodeHandle; 2]> {
|
||||
match self.case().unwrap() {
|
||||
NodeRef::Inner(&InnerNode { children, .. }) => Some(children),
|
||||
NodeRef::Leaf(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn case(&self) -> Option<NodeRef> {
|
||||
match NodeTag::try_from(self.tag) {
|
||||
Ok(NodeTag::InnerNode) => Some(NodeRef::Inner(cast_ref(self))),
|
||||
Ok(NodeTag::LeafNode) => Some(NodeRef::Leaf(cast_ref(self))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn case_mut(&mut self) -> Option<NodeRefMut> {
|
||||
match NodeTag::try_from(self.tag) {
|
||||
Ok(NodeTag::InnerNode) => Some(NodeRefMut::Inner(cast_mut(self))),
|
||||
Ok(NodeTag::LeafNode) => Some(NodeRefMut::Leaf(cast_mut(self))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_leaf(&self) -> Option<&LeafNode> {
|
||||
match self.case() {
|
||||
Some(NodeRef::Leaf(leaf_ref)) => Some(leaf_ref),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_leaf_mut(&mut self) -> Option<&mut LeafNode> {
|
||||
match self.case_mut() {
|
||||
Some(NodeRefMut::Leaf(leaf_ref)) => Some(leaf_ref),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_inner(&self) -> Option<&InnerNode> {
|
||||
match self.case() {
|
||||
Some(NodeRef::Inner(inner_ref)) => Some(inner_ref),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_inner_mut(&mut self) -> Option<&mut InnerNode> {
|
||||
match self.case_mut() {
|
||||
Some(NodeRefMut::Inner(inner_ref)) => Some(inner_ref),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn earliest_expiry(&self) -> u64 {
|
||||
match self.case().unwrap() {
|
||||
NodeRef::Inner(inner) => inner.earliest_expiry(),
|
||||
NodeRef::Leaf(leaf) => leaf.expiry(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<AnyNode> for InnerNode {
|
||||
fn as_ref(&self) -> &AnyNode {
|
||||
cast_ref(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<AnyNode> for LeafNode {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &AnyNode {
|
||||
cast_ref(self)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
use anchor_lang::prelude::Error;
|
||||
use bytemuck::{bytes_of, cast_slice_mut, from_bytes_mut, Contiguous, Pod};
|
||||
|
||||
use solana_program::account_info::AccountInfo;
|
||||
use solana_program::program_error::ProgramError;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use std::cell::RefMut;
|
||||
use std::mem::size_of;
|
||||
|
||||
#[inline]
|
||||
pub fn remove_slop_mut<T: Pod>(bytes: &mut [u8]) -> &mut [T] {
|
||||
let slop = bytes.len() % size_of::<T>();
|
||||
let new_len = bytes.len() - slop;
|
||||
cast_slice_mut(&mut bytes[..new_len])
|
||||
}
|
||||
|
||||
pub fn strip_header_mut<'a, H: Pod, D: Pod>(
|
||||
account: &'a AccountInfo,
|
||||
) -> Result<(RefMut<'a, H>, RefMut<'a, [D]>), Error> {
|
||||
Ok(RefMut::map_split(account.try_borrow_mut_data()?, |data| {
|
||||
let (header_bytes, inner_bytes) = data.split_at_mut(size_of::<H>());
|
||||
(from_bytes_mut(header_bytes), remove_slop_mut(inner_bytes))
|
||||
}))
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
|
||||
#[derive(
|
||||
Eq,
|
||||
PartialEq,
|
||||
Copy,
|
||||
Clone,
|
||||
TryFromPrimitive,
|
||||
IntoPrimitive,
|
||||
Debug,
|
||||
AnchorSerialize,
|
||||
AnchorDeserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum OrderType {
|
||||
/// Take existing orders up to price, max_base_quantity and max_quote_quantity.
|
||||
/// If any base_quantity or quote_quantity remains, place an order on the book
|
||||
Limit = 0,
|
||||
|
||||
/// Take existing orders up to price, max_base_quantity and max_quote_quantity.
|
||||
/// Never place an order on the book.
|
||||
ImmediateOrCancel = 1,
|
||||
|
||||
/// Never take any existing orders, post the order on the book if possible.
|
||||
/// If existing orders can match with this order, do nothing.
|
||||
PostOnly = 2,
|
||||
|
||||
/// Ignore price and take orders up to max_base_quantity and max_quote_quantity.
|
||||
/// Never place an order on the book.
|
||||
///
|
||||
/// Equivalent to ImmediateOrCancel with price=i64::MAX.
|
||||
Market = 3,
|
||||
|
||||
/// If existing orders match with this order, adjust the price to just barely
|
||||
/// not match. Always places an order on the book.
|
||||
PostOnlySlide = 4,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Eq,
|
||||
PartialEq,
|
||||
Copy,
|
||||
Clone,
|
||||
TryFromPrimitive,
|
||||
IntoPrimitive,
|
||||
Debug,
|
||||
AnchorSerialize,
|
||||
AnchorDeserialize,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum Side {
|
||||
Bid = 0,
|
||||
Ask = 1,
|
||||
}
|
|
@ -0,0 +1,275 @@
|
|||
use super::{book::Book, metadata::MetaData, orders::Side};
|
||||
use crate::error::MangoError;
|
||||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
use fixed_macro::types::I80F48;
|
||||
use mango_macro::Pod;
|
||||
|
||||
/// This will hold top level info about the perps market
|
||||
/// Likely all perps transactions on a market will be locked on this one because this will be passed in as writable
|
||||
#[account(zero_copy)]
|
||||
pub struct PerpMarket {
|
||||
pub meta_data: MetaData,
|
||||
|
||||
pub mango_group: Pubkey,
|
||||
pub bids: Pubkey,
|
||||
pub asks: Pubkey,
|
||||
pub event_queue: Pubkey,
|
||||
pub quote_lot_size: i64, // number of quote native that reresents min tick
|
||||
pub base_lot_size: i64, // represents number of base native quantity; greater than 0
|
||||
|
||||
// TODO - consider just moving this into the cache
|
||||
pub long_funding: I80F48,
|
||||
pub short_funding: I80F48,
|
||||
|
||||
pub open_interest: i64, // This is i64 to keep consistent with the units of contracts, but should always be > 0
|
||||
|
||||
pub last_updated: u64,
|
||||
pub seq_num: u64,
|
||||
pub fees_accrued: I80F48, // native quote currency
|
||||
|
||||
pub liquidity_mining_info: LiquidityMiningInfo,
|
||||
|
||||
// mngo_vault holds mango tokens to be disbursed as liquidity incentives for this perp market
|
||||
pub mngo_vault: Pubkey,
|
||||
}
|
||||
|
||||
impl PerpMarket {
|
||||
// pub fn load_and_init<'a>(
|
||||
// account: &'a AccountInfo,
|
||||
// program_id: &Pubkey,
|
||||
// mango_group_ai: &'a AccountInfo,
|
||||
// bids_ai: &'a AccountInfo,
|
||||
// asks_ai: &'a AccountInfo,
|
||||
// event_queue_ai: &'a AccountInfo,
|
||||
// mngo_vault_ai: &'a AccountInfo,
|
||||
// mango_group: &MangoGroup,
|
||||
// rent: &Rent,
|
||||
// base_lot_size: i64,
|
||||
// quote_lot_size: i64,
|
||||
// rate: I80F48,
|
||||
// max_depth_bps: I80F48,
|
||||
// target_period_length: u64,
|
||||
// mngo_per_period: u64,
|
||||
// exp: u8,
|
||||
// version: u8,
|
||||
// lm_size_shift: u8, // right shift the depth number to prevent overflow
|
||||
// ) -> Result<RefMut<'a, Self>> {
|
||||
// let mut state = Self::load_mut(account)?;
|
||||
// check!(account.owner == program_id, MangoErrorCode::InvalidOwner)?;
|
||||
// check!(
|
||||
// rent.is_exempt(account.lamports(), size_of::<Self>()),
|
||||
// MangoErrorCode::AccountNotRentExempt
|
||||
// )?;
|
||||
// check!(!state.meta_data.is_initialized, MangoErrorCode::Default)?;
|
||||
|
||||
// state.meta_data = MetaData::new_with_extra(
|
||||
// DataType::PerpMarket,
|
||||
// version,
|
||||
// true,
|
||||
// [exp, lm_size_shift, 0, 0, 0],
|
||||
// );
|
||||
// state.mango_group = *mango_group_ai.key;
|
||||
// state.bids = *bids_ai.key;
|
||||
// state.asks = *asks_ai.key;
|
||||
// state.event_queue = *event_queue_ai.key;
|
||||
// state.quote_lot_size = quote_lot_size;
|
||||
// state.base_lot_size = base_lot_size;
|
||||
|
||||
// let vault = Account::unpack(&mngo_vault_ai.try_borrow_data()?)?;
|
||||
// check!(
|
||||
// vault.owner == mango_group.signer_key,
|
||||
// MangoErrorCode::InvalidOwner
|
||||
// )?;
|
||||
// check!(vault.delegate.is_none(), MangoErrorCode::InvalidVault)?;
|
||||
// check!(
|
||||
// vault.close_authority.is_none(),
|
||||
// MangoErrorCode::InvalidVault
|
||||
// )?;
|
||||
// check!(vault.mint == mngo_token::ID, MangoErrorCode::InvalidVault)?;
|
||||
// check!(
|
||||
// mngo_vault_ai.owner == &spl_token::ID,
|
||||
// MangoErrorCode::InvalidOwner
|
||||
// )?;
|
||||
// state.mngo_vault = *mngo_vault_ai.key;
|
||||
|
||||
// let clock = Clock::get()?;
|
||||
// let period_start = clock.unix_timestamp as u64;
|
||||
// state.last_updated = period_start;
|
||||
|
||||
// state.liquidity_mining_info = LiquidityMiningInfo {
|
||||
// rate,
|
||||
// max_depth_bps,
|
||||
// period_start,
|
||||
// target_period_length,
|
||||
// mngo_left: mngo_per_period,
|
||||
// mngo_per_period,
|
||||
// };
|
||||
|
||||
// Ok(state)
|
||||
// }
|
||||
|
||||
// pub fn load_checked<'a>(
|
||||
// account: &'a AccountInfo,
|
||||
// program_id: &Pubkey,
|
||||
// mango_group_pk: &Pubkey,
|
||||
// ) -> MangoResult<Ref<'a, Self>> {
|
||||
// check_eq!(account.owner, program_id, MangoErrorCode::InvalidOwner)?;
|
||||
// let state = Self::load(account)?;
|
||||
// check!(state.meta_data.is_initialized, MangoErrorCode::Default)?;
|
||||
// check!(
|
||||
// state.meta_data.data_type == DataType::PerpMarket as u8,
|
||||
// MangoErrorCode::Default
|
||||
// )?;
|
||||
// check!(
|
||||
// mango_group_pk == &state.mango_group,
|
||||
// MangoErrorCode::Default
|
||||
// )?;
|
||||
// Ok(state)
|
||||
// }
|
||||
|
||||
// pub fn load_mut_checked<'a>(
|
||||
// account: &'a AccountInfo,
|
||||
// program_id: &Pubkey,
|
||||
// mango_group_pk: &Pubkey,
|
||||
// ) -> MangoResult<RefMut<'a, Self>> {
|
||||
// check_eq!(account.owner, program_id, MangoErrorCode::InvalidOwner)?;
|
||||
// let state = Self::load_mut(account)?;
|
||||
// check!(
|
||||
// state.meta_data.is_initialized,
|
||||
// MangoErrorCode::InvalidAccountState
|
||||
// )?;
|
||||
// check!(
|
||||
// state.meta_data.data_type == DataType::PerpMarket as u8,
|
||||
// MangoErrorCode::InvalidAccountState
|
||||
// )?;
|
||||
// check!(
|
||||
// mango_group_pk == &state.mango_group,
|
||||
// MangoErrorCode::InvalidAccountState
|
||||
// )?;
|
||||
// Ok(state)
|
||||
// }
|
||||
|
||||
pub fn gen_order_id(&mut self, side: Side, price: i64) -> i128 {
|
||||
self.seq_num += 1;
|
||||
|
||||
let upper = (price as i128) << 64;
|
||||
match side {
|
||||
Side::Bid => upper | (!self.seq_num as i128),
|
||||
Side::Ask => upper | (self.seq_num as i128),
|
||||
}
|
||||
}
|
||||
|
||||
/// Use current order book price and index price to update the instantaneous funding
|
||||
pub fn update_funding(
|
||||
&mut self,
|
||||
mango_group: &MangoGroup,
|
||||
book: &Book,
|
||||
mango_cache: &MangoCache,
|
||||
market_index: usize,
|
||||
now_ts: u64,
|
||||
) -> Result<()> {
|
||||
// Get the index price from cache, ensure it's not outdated
|
||||
let price_cache = &mango_cache.price_cache[market_index];
|
||||
price_cache.check_valid(&mango_group, now_ts)?;
|
||||
|
||||
let index_price = price_cache.price;
|
||||
// hard-coded for now because there's no convenient place to put this; also creates breaking
|
||||
// change if we make this a parameter
|
||||
const IMPACT_QUANTITY: i64 = 100;
|
||||
|
||||
// Get current book price & compare it to index price
|
||||
let bid = book.get_impact_price(Side::Bid, IMPACT_QUANTITY, now_ts);
|
||||
let ask = book.get_impact_price(Side::Ask, IMPACT_QUANTITY, now_ts);
|
||||
|
||||
const MAX_FUNDING: I80F48 = I80F48!(0.05);
|
||||
const MIN_FUNDING: I80F48 = I80F48!(-0.05);
|
||||
|
||||
let diff = match (bid, ask) {
|
||||
(Some(bid), Some(ask)) => {
|
||||
// calculate mid-market rate
|
||||
let book_price = self.lot_to_native_price((bid + ask) / 2);
|
||||
(book_price / index_price - I80F48::ONE).clamp(MIN_FUNDING, MAX_FUNDING)
|
||||
}
|
||||
(Some(_bid), None) => MAX_FUNDING,
|
||||
(None, Some(_ask)) => MIN_FUNDING,
|
||||
(None, None) => I80F48::ZERO,
|
||||
};
|
||||
|
||||
// TODO TEST consider what happens if time_factor is very small. Can funding_delta == 0 when diff != 0?
|
||||
let time_factor = I80F48::from_num(now_ts - self.last_updated) / DAY;
|
||||
let funding_delta: I80F48 = index_price
|
||||
.checked_mul(diff)
|
||||
.unwrap()
|
||||
.checked_mul(I80F48::from_num(self.base_lot_size))
|
||||
.unwrap()
|
||||
.checked_mul(time_factor)
|
||||
.unwrap();
|
||||
|
||||
self.long_funding += funding_delta;
|
||||
self.short_funding += funding_delta;
|
||||
self.last_updated = now_ts;
|
||||
|
||||
// Check if liquidity incentives ought to be paid out and if so pay them out
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert from the price stored on the book to the price used in value calculations
|
||||
pub fn lot_to_native_price(&self, price: i64) -> I80F48 {
|
||||
I80F48::from_num(price)
|
||||
.checked_mul(I80F48::from_num(self.quote_lot_size))
|
||||
.unwrap()
|
||||
.checked_div(I80F48::from_num(self.base_lot_size))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Socialize the loss in this account across all longs and shorts
|
||||
pub fn socialize_loss(
|
||||
&mut self,
|
||||
account: &mut PerpAccount,
|
||||
cache: &mut PerpMarketCache,
|
||||
) -> Result<I80F48> {
|
||||
// TODO convert into only socializing on one side
|
||||
// native USDC per contract open interest
|
||||
let socialized_loss = if self.open_interest == 0 {
|
||||
// This is kind of an unfortunate situation. This means socialized loss occurs on the
|
||||
// last person to call settle_pnl on their profits. Any advice on better mechanism
|
||||
// would be appreciated. Luckily, this will be an extremely rare situation.
|
||||
I80F48::ZERO
|
||||
} else {
|
||||
account
|
||||
.quote_position
|
||||
.checked_div(I80F48::from_num(self.open_interest))
|
||||
.ok_or(MangoError::SomeError)?
|
||||
};
|
||||
account.quote_position = I80F48::ZERO;
|
||||
self.long_funding -= socialized_loss;
|
||||
self.short_funding += socialized_loss;
|
||||
|
||||
cache.short_funding = self.short_funding;
|
||||
cache.long_funding = self.long_funding;
|
||||
Ok(socialized_loss)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Pod)]
|
||||
#[repr(C)]
|
||||
/// Information regarding market maker incentives for a perp market
|
||||
pub struct LiquidityMiningInfo {
|
||||
/// Used to convert liquidity points to MNGO
|
||||
pub rate: I80F48,
|
||||
|
||||
pub max_depth_bps: I80F48, // instead of max depth bps, this should be max num contracts
|
||||
|
||||
/// start timestamp of current liquidity incentive period; gets updated when mngo_left goes to 0
|
||||
pub period_start: u64,
|
||||
|
||||
/// Target time length of a period in seconds
|
||||
pub target_period_length: u64,
|
||||
|
||||
/// Paper MNGO left for this period
|
||||
pub mngo_left: u64,
|
||||
|
||||
/// Total amount of MNGO allocated for current period
|
||||
pub mngo_per_period: u64,
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
use mango_macro::Pod;
|
||||
|
||||
#[derive(Copy, Clone, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct PerpMarketInfo {
|
||||
pub perp_market: Pubkey, // One of these may be empty
|
||||
pub maint_asset_weight: I80F48,
|
||||
pub init_asset_weight: I80F48,
|
||||
pub maint_liab_weight: I80F48,
|
||||
pub init_liab_weight: I80F48,
|
||||
pub liquidation_fee: I80F48,
|
||||
pub maker_fee: I80F48,
|
||||
pub taker_fee: I80F48,
|
||||
pub base_lot_size: i64, // The lot size of the underlying
|
||||
pub quote_lot_size: i64, // min tick
|
||||
}
|
|
@ -0,0 +1,438 @@
|
|||
use std::cell::RefMut;
|
||||
use std::mem::size_of;
|
||||
|
||||
use crate::error::MangoError;
|
||||
use crate::state::orderbook::datatype::DataType;
|
||||
use crate::state::PerpMarket;
|
||||
use anchor_lang::prelude::*;
|
||||
use fixed::types::I80F48;
|
||||
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||
use solana_program::account_info::AccountInfo;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use solana_program::sysvar::rent::Rent;
|
||||
use static_assertions::const_assert_eq;
|
||||
|
||||
// use mango_logs::FillLog;
|
||||
use mango_macro::Pod;
|
||||
|
||||
use super::metadata::MetaData;
|
||||
use super::ob_utils::strip_header_mut;
|
||||
use super::order_type::Side;
|
||||
// use safe_transmute::{self, trivial::TriviallyTransmutable};
|
||||
|
||||
// use crate::error::{check_assert, MangoErrorCode, MangoResult, SourceFileId};
|
||||
// use crate::matching::Side;
|
||||
// use crate::state::{DataType, MetaData, PerpMarket};
|
||||
// use crate::utils::strip_header_mut;
|
||||
|
||||
// Don't want event queue to become single threaded if it's logging liquidations
|
||||
// Most common scenario will be liqors depositing USDC and withdrawing some other token
|
||||
// So tying it to token deposited is not wise
|
||||
// also can't tie it to token withdrawn because during bull market, liqs will be depositing all base tokens and withdrawing quote
|
||||
//
|
||||
|
||||
pub trait QueueHeader: bytemuck::Pod {
|
||||
type Item: bytemuck::Pod + Copy;
|
||||
|
||||
fn head(&self) -> usize;
|
||||
fn set_head(&mut self, value: usize);
|
||||
fn count(&self) -> usize;
|
||||
fn set_count(&mut self, value: usize);
|
||||
|
||||
fn incr_event_id(&mut self);
|
||||
fn decr_event_id(&mut self, n: usize);
|
||||
}
|
||||
|
||||
pub struct Queue<'a, H: QueueHeader> {
|
||||
pub header: RefMut<'a, H>,
|
||||
pub buf: RefMut<'a, [H::Item]>,
|
||||
}
|
||||
|
||||
impl<'a, H: QueueHeader> Queue<'a, H> {
|
||||
pub fn new(header: RefMut<'a, H>, buf: RefMut<'a, [H::Item]>) -> Self {
|
||||
Self { header, buf }
|
||||
}
|
||||
|
||||
pub fn load_mut(account: &'a AccountInfo) -> Result<Self> {
|
||||
let (header, buf) = strip_header_mut::<H, H::Item>(account)?;
|
||||
Ok(Self { header, buf })
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.header.count()
|
||||
}
|
||||
|
||||
pub fn full(&self) -> bool {
|
||||
self.header.count() == self.buf.len()
|
||||
}
|
||||
|
||||
pub fn empty(&self) -> bool {
|
||||
self.header.count() == 0
|
||||
}
|
||||
|
||||
pub fn push_back(&mut self, value: H::Item) -> std::result::Result<(), H::Item> {
|
||||
if self.full() {
|
||||
return Err(value);
|
||||
}
|
||||
let slot = (self.header.head() + self.header.count()) % self.buf.len();
|
||||
self.buf[slot] = value;
|
||||
|
||||
let count = self.header.count();
|
||||
self.header.set_count(count + 1);
|
||||
|
||||
self.header.incr_event_id();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn peek_front(&self) -> Option<&H::Item> {
|
||||
if self.empty() {
|
||||
return None;
|
||||
}
|
||||
Some(&self.buf[self.header.head()])
|
||||
}
|
||||
|
||||
pub fn peek_front_mut(&mut self) -> Option<&mut H::Item> {
|
||||
if self.empty() {
|
||||
return None;
|
||||
}
|
||||
Some(&mut self.buf[self.header.head()])
|
||||
}
|
||||
|
||||
pub fn pop_front(&mut self) -> std::result::Result<H::Item, ()> {
|
||||
if self.empty() {
|
||||
return Err(());
|
||||
}
|
||||
let value = self.buf[self.header.head()];
|
||||
|
||||
let count = self.header.count();
|
||||
self.header.set_count(count - 1);
|
||||
|
||||
let head = self.header.head();
|
||||
self.header.set_head((head + 1) % self.buf.len());
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub fn revert_pushes(&mut self, desired_len: usize) -> Result<()> {
|
||||
require!(desired_len <= self.header.count(), MangoError::SomeError);
|
||||
let len_diff = self.header.count() - desired_len;
|
||||
self.header.set_count(desired_len);
|
||||
self.header.decr_event_id(len_diff);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &H::Item> {
|
||||
QueueIterator {
|
||||
queue: self,
|
||||
index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct QueueIterator<'a, 'b, H: QueueHeader> {
|
||||
queue: &'b Queue<'a, H>,
|
||||
index: usize,
|
||||
}
|
||||
|
||||
impl<'a, 'b, H: QueueHeader> Iterator for QueueIterator<'a, 'b, H> {
|
||||
type Item = &'b H::Item;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.index == self.queue.len() {
|
||||
None
|
||||
} else {
|
||||
let item =
|
||||
&self.queue.buf[(self.queue.header.head() + self.index) % self.queue.buf.len()];
|
||||
self.index += 1;
|
||||
Some(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[account(zero_copy)]
|
||||
pub struct EventQueueHeader {
|
||||
pub meta_data: MetaData,
|
||||
head: usize,
|
||||
count: usize,
|
||||
pub seq_num: usize,
|
||||
}
|
||||
// unsafe impl TriviallyTransmutable for EventQueueHeader {}
|
||||
|
||||
impl QueueHeader for EventQueueHeader {
|
||||
type Item = AnyEvent;
|
||||
|
||||
fn head(&self) -> usize {
|
||||
self.head
|
||||
}
|
||||
fn set_head(&mut self, value: usize) {
|
||||
self.head = value;
|
||||
}
|
||||
fn count(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
fn set_count(&mut self, value: usize) {
|
||||
self.count = value;
|
||||
}
|
||||
fn incr_event_id(&mut self) {
|
||||
self.seq_num += 1;
|
||||
}
|
||||
fn decr_event_id(&mut self, n: usize) {
|
||||
self.seq_num -= n;
|
||||
}
|
||||
}
|
||||
|
||||
pub type EventQueue<'a> = Queue<'a, EventQueueHeader>;
|
||||
|
||||
impl<'a> EventQueue<'a> {
|
||||
pub fn load_mut_checked(
|
||||
account: &'a AccountInfo,
|
||||
program_id: &Pubkey,
|
||||
perp_market: &PerpMarket,
|
||||
) -> Result<Self> {
|
||||
require!(account.owner == program_id, MangoError::SomeError); // MangoErrorCode::InvalidOwner
|
||||
// require!(
|
||||
// &perp_market.event_queue == account.key,
|
||||
// MangoError::SomeError
|
||||
// ); // MangoErrorCode::InvalidAccount
|
||||
Self::load_mut(account)
|
||||
}
|
||||
|
||||
pub fn load_and_init(
|
||||
account: &'a AccountInfo,
|
||||
program_id: &Pubkey,
|
||||
rent: &Rent,
|
||||
) -> Result<Self> {
|
||||
// NOTE: check this first so we can borrow account later
|
||||
require!(
|
||||
rent.is_exempt(account.lamports(), account.data_len()),
|
||||
MangoError::SomeError
|
||||
); //MangoErrorCode::AccountNotRentExempt
|
||||
|
||||
let mut state = Self::load_mut(account)?;
|
||||
require!(account.owner == program_id, MangoError::SomeError); // MangoErrorCode::InvalidOwner
|
||||
|
||||
// require!(
|
||||
// !state.header.meta_data.is_initialized,
|
||||
// MangoError::SomeError
|
||||
// );
|
||||
// state.header.meta_data = MetaData::new(DataType::EventQueue, 0, true);
|
||||
|
||||
Ok(state)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, IntoPrimitive, TryFromPrimitive, Eq, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum EventType {
|
||||
Fill,
|
||||
Out,
|
||||
Liquidate,
|
||||
}
|
||||
|
||||
const EVENT_SIZE: usize = 200;
|
||||
#[derive(Copy, Clone, Debug, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct AnyEvent {
|
||||
pub event_type: u8,
|
||||
pub padding: [u8; EVENT_SIZE - 1],
|
||||
}
|
||||
// unsafe impl TriviallyTransmutable for AnyEvent {}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct FillEvent {
|
||||
pub event_type: u8,
|
||||
pub taker_side: Side, // side from the taker's POV
|
||||
pub maker_slot: u8,
|
||||
pub maker_out: bool, // true if maker order quantity == 0
|
||||
pub version: u8,
|
||||
pub market_fees_applied: bool,
|
||||
pub padding: [u8; 2],
|
||||
pub timestamp: u64,
|
||||
pub seq_num: usize, // note: usize same as u64
|
||||
|
||||
pub maker: Pubkey,
|
||||
pub maker_order_id: i128,
|
||||
pub maker_client_order_id: u64,
|
||||
pub maker_fee: I80F48,
|
||||
|
||||
// The best bid/ask at the time the maker order was placed. Used for liquidity incentives
|
||||
pub best_initial: i64,
|
||||
|
||||
// Timestamp of when the maker order was placed; copied over from the LeafNode
|
||||
pub maker_timestamp: u64,
|
||||
|
||||
pub taker: Pubkey,
|
||||
pub taker_order_id: i128,
|
||||
pub taker_client_order_id: u64,
|
||||
pub taker_fee: I80F48,
|
||||
|
||||
pub price: i64,
|
||||
pub quantity: i64, // number of quote lots
|
||||
}
|
||||
// unsafe impl TriviallyTransmutable for FillEvent {}
|
||||
|
||||
impl FillEvent {
|
||||
pub fn new(
|
||||
taker_side: Side,
|
||||
maker_slot: u8,
|
||||
maker_out: bool,
|
||||
timestamp: u64,
|
||||
seq_num: usize,
|
||||
maker: Pubkey,
|
||||
maker_order_id: i128,
|
||||
maker_client_order_id: u64,
|
||||
maker_fee: I80F48,
|
||||
best_initial: i64,
|
||||
maker_timestamp: u64,
|
||||
|
||||
taker: Pubkey,
|
||||
taker_order_id: i128,
|
||||
taker_client_order_id: u64,
|
||||
taker_fee: I80F48,
|
||||
price: i64,
|
||||
quantity: i64,
|
||||
version: u8,
|
||||
) -> FillEvent {
|
||||
Self {
|
||||
event_type: EventType::Fill as u8,
|
||||
taker_side,
|
||||
maker_slot,
|
||||
maker_out,
|
||||
version,
|
||||
market_fees_applied: true, // Since mango v3.3.5, market fees are adjusted at matching time
|
||||
padding: [0u8; 2],
|
||||
timestamp,
|
||||
seq_num,
|
||||
maker,
|
||||
maker_order_id,
|
||||
maker_client_order_id,
|
||||
maker_fee,
|
||||
best_initial,
|
||||
maker_timestamp,
|
||||
taker,
|
||||
taker_order_id,
|
||||
taker_client_order_id,
|
||||
taker_fee,
|
||||
price,
|
||||
quantity,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base_quote_change(&self, side: Side) -> (i64, i64) {
|
||||
match side {
|
||||
Side::Bid => (
|
||||
self.quantity,
|
||||
-self.price.checked_mul(self.quantity).unwrap(),
|
||||
),
|
||||
Side::Ask => (
|
||||
-self.quantity,
|
||||
self.price.checked_mul(self.quantity).unwrap(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn to_fill_log(&self, mango_group: Pubkey, market_index: usize) -> FillLog {
|
||||
// FillLog {
|
||||
// mango_group,
|
||||
// market_index: market_index as u64,
|
||||
// taker_side: self.taker_side as u8,
|
||||
// maker_slot: self.maker_slot,
|
||||
// maker_out: self.maker_out,
|
||||
// timestamp: self.timestamp,
|
||||
// seq_num: self.seq_num as u64,
|
||||
// maker: self.maker,
|
||||
// maker_order_id: self.maker_order_id,
|
||||
// maker_client_order_id: self.maker_client_order_id,
|
||||
// maker_fee: self.maker_fee.to_bits(),
|
||||
// best_initial: self.best_initial,
|
||||
// maker_timestamp: self.maker_timestamp,
|
||||
// taker: self.taker,
|
||||
// taker_order_id: self.taker_order_id,
|
||||
// taker_client_order_id: self.taker_client_order_id,
|
||||
// taker_fee: self.taker_fee.to_bits(),
|
||||
// price: self.price,
|
||||
// quantity: self.quantity,
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Pod)]
|
||||
#[repr(C)]
|
||||
pub struct OutEvent {
|
||||
pub event_type: u8,
|
||||
pub side: Side,
|
||||
pub slot: u8,
|
||||
padding0: [u8; 5],
|
||||
pub timestamp: u64,
|
||||
pub seq_num: usize,
|
||||
pub owner: Pubkey,
|
||||
pub quantity: i64,
|
||||
padding1: [u8; EVENT_SIZE - 64],
|
||||
}
|
||||
// unsafe impl TriviallyTransmutable for OutEvent {}
|
||||
impl OutEvent {
|
||||
pub fn new(
|
||||
side: Side,
|
||||
slot: u8,
|
||||
timestamp: u64,
|
||||
seq_num: usize,
|
||||
owner: Pubkey,
|
||||
quantity: i64,
|
||||
) -> Self {
|
||||
Self {
|
||||
event_type: EventType::Out.into(),
|
||||
side,
|
||||
slot,
|
||||
padding0: [0; 5],
|
||||
timestamp,
|
||||
seq_num,
|
||||
owner,
|
||||
quantity,
|
||||
padding1: [0; EVENT_SIZE - 64],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Pod)]
|
||||
#[repr(C)]
|
||||
/// Liquidation for the PerpMarket this EventQueue is for
|
||||
pub struct LiquidateEvent {
|
||||
pub event_type: u8,
|
||||
padding0: [u8; 7],
|
||||
pub timestamp: u64,
|
||||
pub seq_num: usize,
|
||||
pub liqee: Pubkey,
|
||||
pub liqor: Pubkey,
|
||||
pub price: I80F48, // oracle price at the time of liquidation
|
||||
pub quantity: i64, // number of contracts that were moved from liqee to liqor
|
||||
pub liquidation_fee: I80F48, // liq fee for this earned for this market
|
||||
padding1: [u8; EVENT_SIZE - 128],
|
||||
}
|
||||
// unsafe impl TriviallyTransmutable for LiquidateEvent {}
|
||||
impl LiquidateEvent {
|
||||
pub fn new(
|
||||
timestamp: u64,
|
||||
seq_num: usize,
|
||||
liqee: Pubkey,
|
||||
liqor: Pubkey,
|
||||
price: I80F48,
|
||||
quantity: i64,
|
||||
liquidation_fee: I80F48,
|
||||
) -> Self {
|
||||
Self {
|
||||
event_type: EventType::Liquidate.into(),
|
||||
padding0: [0u8; 7],
|
||||
timestamp,
|
||||
seq_num,
|
||||
liqee,
|
||||
liqor,
|
||||
price,
|
||||
quantity,
|
||||
liquidation_fee,
|
||||
padding1: [0u8; EVENT_SIZE - 128],
|
||||
}
|
||||
}
|
||||
}
|
||||
const_assert_eq!(size_of::<AnyEvent>(), size_of::<FillEvent>());
|
||||
const_assert_eq!(size_of::<AnyEvent>(), size_of::<OutEvent>());
|
||||
const_assert_eq!(size_of::<AnyEvent>(), size_of::<LiquidateEvent>());
|
|
@ -1,5 +1,6 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
use crate::state::orderbook::order_type::Side;
|
||||
use crate::state::TokenIndex;
|
||||
|
||||
pub type PerpMarketIndex = u16;
|
||||
|
@ -7,81 +8,63 @@ pub type PerpMarketIndex = u16;
|
|||
#[account(zero_copy)]
|
||||
pub struct EventQueue {}
|
||||
|
||||
#[account(zero_copy)]
|
||||
pub struct Book {}
|
||||
|
||||
#[account(zero_copy)]
|
||||
pub struct PerpMarket {
|
||||
// todo
|
||||
/// metadata
|
||||
// pub meta_data: MetaData,
|
||||
|
||||
/// mango group
|
||||
pub group: Pubkey,
|
||||
|
||||
// todo better docs
|
||||
///
|
||||
pub oracle: Pubkey,
|
||||
|
||||
/// order book
|
||||
pub bids: Pubkey,
|
||||
pub asks: Pubkey,
|
||||
|
||||
// todo better docs
|
||||
///
|
||||
pub event_queue: Pubkey,
|
||||
/// Event queue of TODO
|
||||
/// pub event_queue: Pubkey,
|
||||
|
||||
/// number of quote native that reresents min tick
|
||||
/// e.g. base lot size 100, quote lot size 10, then tick i.e. price increment is 10/100 i.e. 1
|
||||
// todo: why signed?
|
||||
/// Number of quote native that reresents min tick
|
||||
/// e.g. when base lot size is 100, and quote lot size is 10, then tick i.e. price increment is 10/100 i.e. 0.1
|
||||
pub quote_lot_size: i64,
|
||||
/// represents number of base native quantity; greater than 0
|
||||
/// e.g. base decimals 6, base lot size 100, base position 10000, then
|
||||
|
||||
/// Represents number of base native quantity
|
||||
/// e.g. if base decimals for underlying asset are 6, base lot size is 100, and base position is 10000, then
|
||||
/// UI position is 1
|
||||
// todo: why signed?
|
||||
pub base_lot_size: i64,
|
||||
|
||||
// todo
|
||||
/// an always increasing number (except in case of socializing losses), incremented by
|
||||
/// funding delta, funding delta is difference between book and index price which needs to be paid every day,
|
||||
/// funding delta is measured per day - per base lots - the larger users position the more funding
|
||||
/// he pays, funding is always paid in quote
|
||||
// pub long_funding: I80F48,
|
||||
// pub short_funding: I80F48,
|
||||
// todo
|
||||
/// timestamp when funding was last updated
|
||||
// pub last_updated: u64,
|
||||
/// pub long_funding: I80F48,
|
||||
/// pub short_funding: I80F48,
|
||||
/// pub funding_last_updated: u64,
|
||||
|
||||
// todo
|
||||
/// This is i64 to keep consistent with the units of contracts, but should always be > 0
|
||||
// todo: why signed?
|
||||
// pub open_interest: i64,
|
||||
/// pub open_interest: u64,
|
||||
|
||||
// todo
|
||||
/// number of orders generated
|
||||
/// Total number of orders seen
|
||||
pub seq_num: u64,
|
||||
|
||||
// todo
|
||||
/// in native quote currency
|
||||
// pub fees_accrued: I80F48,
|
||||
/// Fees accrued in native quote currency
|
||||
/// pub fees_accrued: I80F48,
|
||||
|
||||
// todo
|
||||
/// liquidity mining
|
||||
// pub liquidity_mining_info: LiquidityMiningInfo,
|
||||
/// Liquidity mining metadata
|
||||
/// pub liquidity_mining_info: LiquidityMiningInfo,
|
||||
|
||||
// todo
|
||||
/// token vault which holds mango tokens to be disbursed as liquidity incentives for this perp market
|
||||
// pub mngo_vault: Pubkey,
|
||||
/// Token vault which holds mango tokens to be disbursed as liquidity incentives for this perp market
|
||||
/// pub mngo_vault: Pubkey,
|
||||
|
||||
/// pda bump
|
||||
/// PDA bump
|
||||
pub bump: u8,
|
||||
|
||||
/// useful for looking up respective perp account
|
||||
/// Lookup indices
|
||||
pub perp_market_index: PerpMarketIndex,
|
||||
/// useful for looking up respective base token,
|
||||
/// note: is optional, since perp market can exist without a corresponding base token,
|
||||
/// should be TokenIndex::MAX in that case
|
||||
pub base_token_index: TokenIndex,
|
||||
/// useful for looking up respective quote token
|
||||
pub quote_token_index: TokenIndex,
|
||||
}
|
||||
|
||||
impl PerpMarket {
|
||||
/// TODO why is this based on price?
|
||||
pub fn gen_order_id(&mut self, side: Side, price: i64) -> i128 {
|
||||
self.seq_num += 1;
|
||||
|
||||
let upper = (price as i128) << 64;
|
||||
match side {
|
||||
Side::Bid => upper | (!self.seq_num as i128),
|
||||
Side::Ask => upper | (self.seq_num as i128),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::ZeroCopy;
|
||||
use arrayref::array_ref;
|
||||
use std::cell::RefMut;
|
||||
use std::{cell::Ref, mem};
|
||||
|
||||
#[macro_export]
|
||||
|
@ -27,11 +28,34 @@ pub trait LoadZeroCopy {
|
|||
/// It checks the account owner and discriminator, then casts the data.
|
||||
fn load<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>>;
|
||||
|
||||
/// Same as load(), but mut
|
||||
fn load_mut<T: ZeroCopy + Owner>(&self) -> Result<RefMut<T>>;
|
||||
|
||||
/// Same as load(), but doesn't check the discriminator.
|
||||
fn load_unchecked<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>>;
|
||||
|
||||
/// Same as load_unchecked(), but mut
|
||||
fn load_unchecked_mut<T: ZeroCopy + Owner>(&self) -> Result<RefMut<T>>;
|
||||
}
|
||||
|
||||
impl<'info> LoadZeroCopy for AccountInfo<'info> {
|
||||
fn load_mut<T: ZeroCopy + Owner>(&self) -> Result<RefMut<T>> {
|
||||
if self.owner != &T::owner() {
|
||||
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
|
||||
}
|
||||
|
||||
let data = self.try_borrow_mut_data()?;
|
||||
|
||||
let disc_bytes = array_ref![data, 0, 8];
|
||||
if disc_bytes != &T::discriminator() {
|
||||
return Err(ErrorCode::AccountDiscriminatorMismatch.into());
|
||||
}
|
||||
|
||||
Ok(RefMut::map(data, |data| {
|
||||
bytemuck::from_bytes_mut(&mut data[8..mem::size_of::<T>() + 8])
|
||||
}))
|
||||
}
|
||||
|
||||
fn load<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>> {
|
||||
if self.owner != &T::owner() {
|
||||
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
|
||||
|
@ -49,6 +73,18 @@ impl<'info> LoadZeroCopy for AccountInfo<'info> {
|
|||
}))
|
||||
}
|
||||
|
||||
fn load_unchecked_mut<T: ZeroCopy + Owner>(&self) -> Result<RefMut<T>> {
|
||||
if self.owner != &T::owner() {
|
||||
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
|
||||
}
|
||||
|
||||
let data = self.try_borrow_mut_data()?;
|
||||
|
||||
Ok(RefMut::map(data, |data| {
|
||||
bytemuck::from_bytes_mut(&mut data[8..mem::size_of::<T>() + 8])
|
||||
}))
|
||||
}
|
||||
|
||||
fn load_unchecked<T: ZeroCopy + Owner>(&self) -> Result<Ref<T>> {
|
||||
if self.owner != &T::owner() {
|
||||
return Err(ErrorCode::AccountOwnedByWrongProgram.into());
|
||||
|
|
|
@ -1076,8 +1076,10 @@ impl ClientInstruction for Serum3LiqForceCancelOrdersInstruction {
|
|||
|
||||
pub struct CreatePerpMarketInstruction<'keypair> {
|
||||
pub group: Pubkey,
|
||||
pub mint: Pubkey,
|
||||
pub admin: &'keypair Keypair,
|
||||
pub oracle: Pubkey,
|
||||
pub asks: Pubkey,
|
||||
pub bids: Pubkey,
|
||||
pub payer: &'keypair Keypair,
|
||||
pub perp_market_index: PerpMarketIndex,
|
||||
pub base_token_index: TokenIndex,
|
||||
|
@ -1102,12 +1104,6 @@ impl<'keypair> ClientInstruction for CreatePerpMarketInstruction<'keypair> {
|
|||
base_lot_size: self.base_lot_size,
|
||||
};
|
||||
|
||||
let oracle = Pubkey::find_program_address(
|
||||
&[b"StubOracle".as_ref(), self.mint.as_ref()],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
|
||||
let perp_market = Pubkey::find_program_address(
|
||||
&[
|
||||
self.group.as_ref(),
|
||||
|
@ -1117,34 +1113,14 @@ impl<'keypair> ClientInstruction for CreatePerpMarketInstruction<'keypair> {
|
|||
&program_id,
|
||||
)
|
||||
.0;
|
||||
let asks = Pubkey::find_program_address(
|
||||
&[self.group.as_ref(), b"Asks".as_ref(), perp_market.as_ref()],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
let bids = Pubkey::find_program_address(
|
||||
&[self.group.as_ref(), b"Bids".as_ref(), perp_market.as_ref()],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
let event_queue = Pubkey::find_program_address(
|
||||
&[
|
||||
self.group.as_ref(),
|
||||
b"EventQueue".as_ref(),
|
||||
perp_market.as_ref(),
|
||||
],
|
||||
&program_id,
|
||||
)
|
||||
.0;
|
||||
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
admin: self.admin.pubkey(),
|
||||
oracle,
|
||||
oracle: self.oracle,
|
||||
perp_market,
|
||||
asks,
|
||||
bids,
|
||||
event_queue,
|
||||
asks: self.asks,
|
||||
bids: self.bids,
|
||||
payer: self.payer.pubkey(),
|
||||
system_program: System::id(),
|
||||
};
|
||||
|
@ -1157,3 +1133,51 @@ impl<'keypair> ClientInstruction for CreatePerpMarketInstruction<'keypair> {
|
|||
vec![self.admin, self.payer]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlacePerpOrderInstruction<'keypair> {
|
||||
pub group: Pubkey,
|
||||
pub account: Pubkey,
|
||||
pub perp_market: Pubkey,
|
||||
pub asks: Pubkey,
|
||||
pub bids: Pubkey,
|
||||
pub oracle: Pubkey,
|
||||
pub owner: &'keypair Keypair,
|
||||
}
|
||||
#[async_trait::async_trait(?Send)]
|
||||
impl<'keypair> ClientInstruction for PlacePerpOrderInstruction<'keypair> {
|
||||
type Accounts = mango_v4::accounts::PlacePerpOrder;
|
||||
type Instruction = mango_v4::instruction::PlacePerpOrder;
|
||||
async fn to_instruction(
|
||||
&self,
|
||||
_loader: impl ClientAccountLoader + 'async_trait,
|
||||
) -> (Self::Accounts, instruction::Instruction) {
|
||||
let program_id = mango_v4::id();
|
||||
let instruction = Self::Instruction {
|
||||
side: Side::Bid,
|
||||
price: 1,
|
||||
max_base_quantity: 1,
|
||||
max_quote_quantity: 1,
|
||||
client_order_id: 0,
|
||||
order_type: OrderType::Limit,
|
||||
reduce_only: false,
|
||||
expiry_timestamp: 0,
|
||||
limit: 1,
|
||||
};
|
||||
let accounts = Self::Accounts {
|
||||
group: self.group,
|
||||
account: self.account,
|
||||
perp_market: self.perp_market,
|
||||
asks: self.asks,
|
||||
bids: self.bids,
|
||||
oracle: self.oracle,
|
||||
owner: self.owner.pubkey(),
|
||||
};
|
||||
|
||||
let instruction = make_instruction(program_id, &accounts, instruction);
|
||||
(accounts, instruction)
|
||||
}
|
||||
|
||||
fn signers(&self) -> Vec<&Keypair> {
|
||||
vec![self.owner]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -83,6 +83,23 @@ impl SolanaCookie {
|
|||
.newest()
|
||||
}
|
||||
|
||||
pub async fn create_account<T>(&self, owner: &Pubkey) -> Pubkey {
|
||||
let key = Keypair::new();
|
||||
let len = 8 + std::mem::size_of::<T>();
|
||||
let rent = self.rent.minimum_balance(len);
|
||||
let create_account_instr = solana_sdk::system_instruction::create_account(
|
||||
&self.context.borrow().payer.pubkey(),
|
||||
&key.pubkey(),
|
||||
rent,
|
||||
len as u64,
|
||||
&owner,
|
||||
);
|
||||
self.process_transaction(&[create_account_instr], Some(&[&key]))
|
||||
.await
|
||||
.unwrap();
|
||||
key.pubkey()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn create_token_account(&self, owner: &Pubkey, mint: Pubkey) -> Pubkey {
|
||||
let keypair = Keypair::new();
|
||||
|
@ -145,6 +162,16 @@ impl SolanaCookie {
|
|||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_account_opt<T: AccountDeserialize>(&self, address: Pubkey) -> Option<T> {
|
||||
let account = self
|
||||
.context
|
||||
.borrow_mut()
|
||||
.banks_client
|
||||
.get_account(address)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
println!("{:#?}", account.owner);
|
||||
|
||||
let data = self.get_account_data(address).await?;
|
||||
let mut data_slice: &[u8] = &data;
|
||||
AccountDeserialize::try_deserialize(&mut data_slice).ok()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use mango_v4::state::BookSide;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{signature::Keypair, transport::TransportError};
|
||||
|
||||
|
@ -77,13 +78,26 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
//
|
||||
// TEST: Create a perp market
|
||||
//
|
||||
let _perp_market = send_tx(
|
||||
let mango_v4::accounts::CreatePerpMarket {
|
||||
perp_market,
|
||||
asks,
|
||||
bids,
|
||||
..
|
||||
} = send_tx(
|
||||
solana,
|
||||
CreatePerpMarketInstruction {
|
||||
group,
|
||||
oracle: tokens[0].oracle,
|
||||
asks: context
|
||||
.solana
|
||||
.create_account::<BookSide>(&mango_v4::id())
|
||||
.await,
|
||||
bids: context
|
||||
.solana
|
||||
.create_account::<BookSide>(&mango_v4::id())
|
||||
.await,
|
||||
admin,
|
||||
payer,
|
||||
mint: mints[0].pubkey,
|
||||
perp_market_index: 0,
|
||||
base_token_index: tokens[0].index,
|
||||
quote_token_index: tokens[1].index,
|
||||
|
@ -93,8 +107,22 @@ async fn test_perp() -> Result<(), TransportError> {
|
|||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.perp_market;
|
||||
.unwrap();
|
||||
|
||||
send_tx(
|
||||
solana,
|
||||
PlacePerpOrderInstruction {
|
||||
group,
|
||||
account,
|
||||
perp_market,
|
||||
asks,
|
||||
bids,
|
||||
oracle: tokens[0].oracle,
|
||||
owner,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue