use borsh::to_vec;
use solana_pubkey::Pubkey;
use crate::ix::constants::{
PHOENIX_GLOBAL_CONFIGURATION, PHOENIX_LOG_AUTHORITY, PHOENIX_PROGRAM_ID,
place_multi_limit_order_discriminant,
};
use crate::ix::error::PhoenixIxError;
use crate::ix::order_packet::{CondensedOrder, MultipleOrderPacket, client_order_id_to_bytes};
use crate::ix::types::{AccountMeta, Instruction};
#[derive(Debug, Clone)]
pub struct MultiLimitOrderParams {
trader: Pubkey,
trader_account: Pubkey,
perp_asset_map: Pubkey,
orderbook: Pubkey,
spline_collection: Pubkey,
global_trader_index: Vec<Pubkey>,
active_trader_buffer: Vec<Pubkey>,
bids: Vec<CondensedOrder>,
asks: Vec<CondensedOrder>,
client_order_id: Option<u128>,
slide: bool,
symbol: String,
}
impl MultiLimitOrderParams {
pub fn builder() -> MultiLimitOrderParamsBuilder {
MultiLimitOrderParamsBuilder::new()
}
pub fn trader(&self) -> Pubkey {
self.trader
}
pub fn trader_account(&self) -> Pubkey {
self.trader_account
}
pub fn perp_asset_map(&self) -> Pubkey {
self.perp_asset_map
}
pub fn orderbook(&self) -> Pubkey {
self.orderbook
}
pub fn spline_collection(&self) -> Pubkey {
self.spline_collection
}
pub fn global_trader_index(&self) -> &[Pubkey] {
&self.global_trader_index
}
pub fn active_trader_buffer(&self) -> &[Pubkey] {
&self.active_trader_buffer
}
pub fn bids(&self) -> &[CondensedOrder] {
&self.bids
}
pub fn asks(&self) -> &[CondensedOrder] {
&self.asks
}
pub fn client_order_id(&self) -> Option<u128> {
self.client_order_id
}
pub fn slide(&self) -> bool {
self.slide
}
pub fn symbol(&self) -> &str {
&self.symbol
}
}
#[derive(Default)]
pub struct MultiLimitOrderParamsBuilder {
trader: Option<Pubkey>,
trader_account: Option<Pubkey>,
perp_asset_map: Option<Pubkey>,
orderbook: Option<Pubkey>,
spline_collection: Option<Pubkey>,
global_trader_index: Option<Vec<Pubkey>>,
active_trader_buffer: Option<Vec<Pubkey>>,
bids: Vec<CondensedOrder>,
asks: Vec<CondensedOrder>,
client_order_id: Option<u128>,
slide: bool,
symbol: Option<String>,
}
impl MultiLimitOrderParamsBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn trader(mut self, trader: Pubkey) -> Self {
self.trader = Some(trader);
self
}
pub fn trader_account(mut self, trader_account: Pubkey) -> Self {
self.trader_account = Some(trader_account);
self
}
pub fn perp_asset_map(mut self, perp_asset_map: Pubkey) -> Self {
self.perp_asset_map = Some(perp_asset_map);
self
}
pub fn orderbook(mut self, orderbook: Pubkey) -> Self {
self.orderbook = Some(orderbook);
self
}
pub fn spline_collection(mut self, spline_collection: Pubkey) -> Self {
self.spline_collection = Some(spline_collection);
self
}
pub fn global_trader_index(mut self, global_trader_index: Vec<Pubkey>) -> Self {
self.global_trader_index = Some(global_trader_index);
self
}
pub fn active_trader_buffer(mut self, active_trader_buffer: Vec<Pubkey>) -> Self {
self.active_trader_buffer = Some(active_trader_buffer);
self
}
pub fn bids(mut self, bids: Vec<CondensedOrder>) -> Self {
self.bids = bids;
self
}
pub fn asks(mut self, asks: Vec<CondensedOrder>) -> Self {
self.asks = asks;
self
}
pub fn add_bid(mut self, order: CondensedOrder) -> Self {
self.bids.push(order);
self
}
pub fn add_ask(mut self, order: CondensedOrder) -> Self {
self.asks.push(order);
self
}
pub fn client_order_id(mut self, client_order_id: u128) -> Self {
self.client_order_id = Some(client_order_id);
self
}
pub fn slide(mut self, slide: bool) -> Self {
self.slide = slide;
self
}
pub fn symbol(mut self, symbol: impl Into<String>) -> Self {
self.symbol = Some(symbol.into());
self
}
pub fn build(self) -> Result<MultiLimitOrderParams, PhoenixIxError> {
Ok(MultiLimitOrderParams {
trader: self.trader.ok_or(PhoenixIxError::MissingField("trader"))?,
trader_account: self
.trader_account
.ok_or(PhoenixIxError::MissingField("trader_account"))?,
perp_asset_map: self
.perp_asset_map
.ok_or(PhoenixIxError::MissingField("perp_asset_map"))?,
orderbook: self
.orderbook
.ok_or(PhoenixIxError::MissingField("orderbook"))?,
spline_collection: self
.spline_collection
.ok_or(PhoenixIxError::MissingField("spline_collection"))?,
global_trader_index: self
.global_trader_index
.ok_or(PhoenixIxError::MissingField("global_trader_index"))?,
active_trader_buffer: self
.active_trader_buffer
.ok_or(PhoenixIxError::MissingField("active_trader_buffer"))?,
bids: self.bids,
asks: self.asks,
client_order_id: self.client_order_id,
slide: self.slide,
symbol: self.symbol.unwrap_or_default(),
})
}
}
pub fn create_place_multi_limit_order_ix(
params: MultiLimitOrderParams,
) -> Result<Instruction, PhoenixIxError> {
validate(¶ms)?;
let data = encode_multi_limit_order(¶ms);
let accounts = build_accounts(¶ms);
Ok(Instruction {
program_id: PHOENIX_PROGRAM_ID,
accounts,
data,
})
}
fn validate(params: &MultiLimitOrderParams) -> Result<(), PhoenixIxError> {
if params.global_trader_index().is_empty() {
return Err(PhoenixIxError::EmptyGlobalTraderIndex);
}
if params.active_trader_buffer().is_empty() {
return Err(PhoenixIxError::EmptyActiveTraderBuffer);
}
Ok(())
}
fn encode_multi_limit_order(params: &MultiLimitOrderParams) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&place_multi_limit_order_discriminant());
let client_order_id = params.client_order_id().map(client_order_id_to_bytes);
let packet = MultipleOrderPacket {
bids: params.bids().to_vec(),
asks: params.asks().to_vec(),
client_order_id,
slide: params.slide(),
};
data.extend_from_slice(&to_vec(&packet).expect("serialization should not fail"));
data
}
fn build_accounts(params: &MultiLimitOrderParams) -> Vec<AccountMeta> {
let mut accounts = Vec::new();
accounts.push(AccountMeta::readonly(PHOENIX_PROGRAM_ID));
accounts.push(AccountMeta::readonly(PHOENIX_LOG_AUTHORITY));
accounts.push(AccountMeta::writable(PHOENIX_GLOBAL_CONFIGURATION));
accounts.push(AccountMeta::readonly_signer(params.trader()));
accounts.push(AccountMeta::writable(params.trader_account()));
accounts.push(AccountMeta::writable(params.perp_asset_map()));
for addr in params.global_trader_index() {
accounts.push(AccountMeta::writable(*addr));
}
for addr in params.active_trader_buffer() {
accounts.push(AccountMeta::writable(*addr));
}
accounts.push(AccountMeta::writable(params.orderbook()));
accounts.push(AccountMeta::writable(params.spline_collection()));
accounts
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_multi_limit_order_ix() {
let params = MultiLimitOrderParams::builder()
.trader(Pubkey::new_unique())
.trader_account(Pubkey::new_unique())
.perp_asset_map(Pubkey::new_unique())
.orderbook(Pubkey::new_unique())
.spline_collection(Pubkey::new_unique())
.global_trader_index(vec![Pubkey::new_unique()])
.active_trader_buffer(vec![Pubkey::new_unique()])
.add_bid(CondensedOrder {
price_in_ticks: 50000,
size_in_base_lots: 1000,
last_valid_slot: None,
})
.add_ask(CondensedOrder {
price_in_ticks: 51000,
size_in_base_lots: 1000,
last_valid_slot: None,
})
.slide(true)
.build()
.unwrap();
let ix = create_place_multi_limit_order_ix(params).unwrap();
assert_eq!(ix.program_id, PHOENIX_PROGRAM_ID);
assert_eq!(ix.accounts.len(), 10);
assert_eq!(&ix.data[..8], &place_multi_limit_order_discriminant());
}
#[test]
fn test_empty_orders_allowed() {
let params = MultiLimitOrderParams::builder()
.trader(Pubkey::new_unique())
.trader_account(Pubkey::new_unique())
.perp_asset_map(Pubkey::new_unique())
.orderbook(Pubkey::new_unique())
.spline_collection(Pubkey::new_unique())
.global_trader_index(vec![Pubkey::new_unique()])
.active_trader_buffer(vec![Pubkey::new_unique()])
.build()
.unwrap();
let ix = create_place_multi_limit_order_ix(params);
assert!(ix.is_ok());
}
#[test]
fn test_empty_global_trader_index_fails() {
let params = MultiLimitOrderParams::builder()
.trader(Pubkey::new_unique())
.trader_account(Pubkey::new_unique())
.perp_asset_map(Pubkey::new_unique())
.orderbook(Pubkey::new_unique())
.spline_collection(Pubkey::new_unique())
.global_trader_index(vec![])
.active_trader_buffer(vec![Pubkey::new_unique()])
.build()
.unwrap();
let result = create_place_multi_limit_order_ix(params);
assert!(matches!(
result,
Err(PhoenixIxError::EmptyGlobalTraderIndex)
));
}
#[test]
fn test_builder_missing_required_field() {
let result = MultiLimitOrderParams::builder()
.trader(Pubkey::new_unique())
.build();
assert!(matches!(result, Err(PhoenixIxError::MissingField(_))));
}
}