phoenix/program/processor/
cancel_multiple_orders.rs

1use crate::{
2    program::{
3        assert_with_msg, dispatch_market::load_with_dispatch_mut,
4        loaders::CancelOrWithdrawContext as Cancel, token_utils::try_withdraw,
5        validation::checkers::phoenix_checkers::MarketAccountInfo, MarketHeader, PhoenixError,
6        PhoenixMarketContext, PhoenixVaultContext,
7    },
8    quantities::{Ticks, WrapperU64},
9    state::{
10        markets::{FIFOOrderId, MarketEvent},
11        MatchingEngineResponse, Side,
12    },
13};
14use borsh::{BorshDeserialize, BorshSerialize};
15use solana_program::{
16    account_info::AccountInfo, entrypoint::ProgramResult, log::sol_log_compute_units,
17    pubkey::Pubkey,
18};
19use std::mem::size_of;
20
21use super::CancelOrderParams;
22
23#[derive(BorshDeserialize, BorshSerialize, Clone, Copy)]
24pub struct CancelUpToParams {
25    pub side: Side,
26    pub tick_limit: Option<u64>,
27    pub num_orders_to_search: Option<u32>,
28    pub num_orders_to_cancel: Option<u32>,
29}
30
31#[derive(BorshDeserialize, BorshSerialize, Clone)]
32pub struct CancelMultipleOrdersByIdParams {
33    pub orders: Vec<CancelOrderParams>,
34}
35
36pub(crate) fn process_cancel_all_orders<'a, 'info>(
37    _program_id: &Pubkey,
38    market_context: &PhoenixMarketContext<'a, 'info>,
39    accounts: &'a [AccountInfo<'info>],
40    _data: &[u8],
41    withdraw_funds: bool,
42    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
43) -> ProgramResult {
44    let vault_context_option = if withdraw_funds {
45        let Cancel { vault_context } = Cancel::load(market_context, accounts)?;
46        Some(vault_context)
47    } else {
48        None
49    };
50
51    let PhoenixMarketContext {
52        market_info,
53        signer: trader,
54    } = market_context;
55
56    let claim_funds = vault_context_option.is_some();
57    let MatchingEngineResponse {
58        num_base_lots_out,
59        num_quote_lots_out,
60        ..
61    } = {
62        let market_bytes = &mut market_info.try_borrow_mut_data()?[size_of::<MarketHeader>()..];
63        let market = load_with_dispatch_mut(&market_info.size_params, market_bytes)?.inner;
64        sol_log_compute_units();
65        market
66            .cancel_all_orders(trader.key, claim_funds, record_event_fn)
67            .unwrap_or_default()
68    };
69    sol_log_compute_units();
70
71    let header = market_info.get_header()?;
72
73    if let Some(PhoenixVaultContext {
74        base_account,
75        quote_account,
76        base_vault,
77        quote_vault,
78        token_program,
79    }) = vault_context_option
80    {
81        try_withdraw(
82            market_info.key,
83            &header.base_params,
84            &header.quote_params,
85            &token_program,
86            quote_account.as_ref(),
87            quote_vault,
88            base_account.as_ref(),
89            base_vault,
90            num_quote_lots_out * header.get_quote_lot_size(),
91            num_base_lots_out * header.get_base_lot_size(),
92        )?;
93    } else {
94        // This case is only reached if the user is cancelling orders with free funds
95        // In this case, there should be no funds to claim
96        assert_with_msg(
97            num_quote_lots_out == 0,
98            PhoenixError::CancelMultipleOrdersError,
99            "WARNING: num_quote_lots_out must be 0",
100        )?;
101        assert_with_msg(
102            num_base_lots_out == 0,
103            PhoenixError::CancelMultipleOrdersError,
104            "WARNING: num_base_lots_out must be 0",
105        )?;
106    }
107
108    drop(header);
109    Ok(())
110}
111
112pub(crate) fn process_cancel_up_to<'a, 'info>(
113    _program_id: &Pubkey,
114    market_context: &PhoenixMarketContext<'a, 'info>,
115    accounts: &'a [AccountInfo<'info>],
116    data: &[u8],
117    withdraw_funds: bool,
118    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
119) -> ProgramResult {
120    let vault_context_option = if withdraw_funds {
121        let Cancel { vault_context } = Cancel::load(market_context, accounts)?;
122        Some(vault_context)
123    } else {
124        None
125    };
126
127    let PhoenixMarketContext {
128        market_info,
129        signer: trader,
130    } = market_context;
131
132    let params = CancelUpToParams::try_from_slice(data)?;
133    process_cancel_orders(
134        market_info,
135        trader.key,
136        vault_context_option,
137        params,
138        record_event_fn,
139    )
140}
141
142pub(crate) fn process_cancel_multiple_orders_by_id<'a, 'info>(
143    _program_id: &Pubkey,
144    market_context: &PhoenixMarketContext<'a, 'info>,
145    accounts: &'a [AccountInfo<'info>],
146    data: &[u8],
147    withdraw_funds: bool,
148    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
149) -> ProgramResult {
150    let vault_context_option = if withdraw_funds {
151        let Cancel { vault_context } = Cancel::load(market_context, accounts)?;
152        Some(vault_context)
153    } else {
154        None
155    };
156
157    let PhoenixMarketContext {
158        market_info,
159        signer: trader,
160    } = market_context;
161
162    let cancel_params = CancelMultipleOrdersByIdParams::try_from_slice(data)?;
163    if cancel_params.orders.is_empty() {
164        phoenix_log!("No orders to cancel");
165        return Ok(());
166    }
167
168    let MatchingEngineResponse {
169        num_quote_lots_out,
170        num_base_lots_out,
171        ..
172    } = {
173        sol_log_compute_units();
174        let market_bytes = &mut market_info.try_borrow_mut_data()?[size_of::<MarketHeader>()..];
175        let market = load_with_dispatch_mut(&market_info.size_params, market_bytes)?.inner;
176        let orders_to_cancel = cancel_params
177            .orders
178            .iter()
179            .filter_map(
180                |CancelOrderParams {
181                     side,
182                     price_in_ticks,
183                     order_sequence_number,
184                 }| {
185                    if *side == Side::from_order_sequence_number(*order_sequence_number) {
186                        Some(FIFOOrderId::new(
187                            Ticks::new(*price_in_ticks),
188                            *order_sequence_number,
189                        ))
190                    } else {
191                        None
192                    }
193                },
194            )
195            .collect::<Vec<_>>();
196
197        market
198            .cancel_multiple_orders_by_id(
199                trader.key,
200                &orders_to_cancel,
201                vault_context_option.is_some(),
202                record_event_fn,
203            )
204            .unwrap_or_default()
205    };
206    sol_log_compute_units();
207
208    let header = market_info.get_header()?;
209
210    if let Some(PhoenixVaultContext {
211        base_account,
212        quote_account,
213        base_vault,
214        quote_vault,
215        token_program,
216    }) = vault_context_option
217    {
218        try_withdraw(
219            market_info.key,
220            &header.base_params,
221            &header.quote_params,
222            &token_program,
223            quote_account.as_ref(),
224            quote_vault,
225            base_account.as_ref(),
226            base_vault,
227            num_quote_lots_out * header.get_quote_lot_size(),
228            num_base_lots_out * header.get_base_lot_size(),
229        )?;
230    } else {
231        // This case is only reached if the user is cancelling orders with free funds
232        // In this case, there should be no funds to claim
233        assert_with_msg(
234            num_quote_lots_out == 0,
235            PhoenixError::CancelMultipleOrdersError,
236            "WARNING: num_quote_lots_out must be 0",
237        )?;
238        assert_with_msg(
239            num_base_lots_out == 0,
240            PhoenixError::CancelMultipleOrdersError,
241            "WARNING: num_base_lots_out must be 0",
242        )?;
243    }
244
245    Ok(())
246}
247
248#[allow(clippy::too_many_arguments)]
249pub(crate) fn process_cancel_orders<'a, 'info>(
250    market_info: &MarketAccountInfo<'a, 'info>,
251    trader_key: &Pubkey,
252    vault_context_option: Option<PhoenixVaultContext<'a, 'info>>,
253    cancel_params: CancelUpToParams,
254    record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
255) -> ProgramResult {
256    let CancelUpToParams {
257        side,
258        tick_limit,
259        num_orders_to_search,
260        num_orders_to_cancel,
261    } = cancel_params;
262
263    let claim_funds = vault_context_option.is_some();
264    let released = {
265        let market_bytes = &mut market_info.try_borrow_mut_data()?[size_of::<MarketHeader>()..];
266        let market = load_with_dispatch_mut(&market_info.size_params, market_bytes)?.inner;
267        sol_log_compute_units();
268        market
269            .cancel_up_to(
270                trader_key,
271                side,
272                num_orders_to_search.map(|x| x as usize),
273                num_orders_to_cancel.map(|x| x as usize),
274                tick_limit.map(Ticks::new),
275                claim_funds,
276                record_event_fn,
277            )
278            .unwrap_or_default()
279    };
280    sol_log_compute_units();
281
282    let header = market_info.get_header()?;
283
284    let MatchingEngineResponse {
285        num_quote_lots_out,
286        num_base_lots_out,
287        ..
288    } = released;
289    if let Some(PhoenixVaultContext {
290        base_account,
291        quote_account,
292        base_vault,
293        quote_vault,
294        token_program,
295    }) = vault_context_option
296    {
297        try_withdraw(
298            market_info.key,
299            &header.base_params,
300            &header.quote_params,
301            &token_program,
302            quote_account.as_ref(),
303            quote_vault,
304            base_account.as_ref(),
305            base_vault,
306            num_quote_lots_out * header.get_quote_lot_size(),
307            num_base_lots_out * header.get_base_lot_size(),
308        )?;
309    } else {
310        // This case is only reached if the user is cancelling orders with free funds
311        // In this case, there should be no funds to claim
312        assert_with_msg(
313            num_quote_lots_out == 0,
314            PhoenixError::CancelMultipleOrdersError,
315            "WARNING: num_quote_lots_out must be 0",
316        )?;
317        assert_with_msg(
318            num_base_lots_out == 0,
319            PhoenixError::CancelMultipleOrdersError,
320            "WARNING: num_base_lots_out must be 0",
321        )?;
322    }
323
324    Ok(())
325}