1use crate::{
2 program::{
3 dispatch_market::load_with_dispatch_mut,
4 error::{assert_with_msg, PhoenixError},
5 loaders::NewOrderContext,
6 status::MarketStatus,
7 token_utils::{maybe_invoke_deposit, maybe_invoke_withdraw},
8 MarketHeader, PhoenixMarketContext, PhoenixVaultContext,
9 },
10 quantities::{
11 BaseAtoms, BaseAtomsPerBaseLot, BaseLots, QuoteAtoms, QuoteAtomsPerQuoteLot, QuoteLots,
12 Ticks, WrapperU64,
13 },
14 state::{
15 decode_order_packet,
16 markets::{FIFOOrderId, FIFORestingOrder, MarketEvent, MarketWrapperMut},
17 OrderPacket, OrderPacketMetadata, Side,
18 },
19};
20use borsh::{BorshDeserialize, BorshSerialize};
21use itertools::Itertools;
22use solana_program::{
23 account_info::AccountInfo, clock::Clock, entrypoint::ProgramResult, log::sol_log_compute_units,
24 program_error::ProgramError, pubkey::Pubkey, sysvar::Sysvar,
25};
26use std::mem::size_of;
27
28#[derive(BorshDeserialize, BorshSerialize, Debug)]
29pub enum FailedMultipleLimitOrderBehavior {
30 FailOnInsufficientFundsAndAmendOnCross,
35
36 FailOnInsufficientFundsAndFailOnCross,
38
39 SkipOnInsufficientFundsAndAmendOnCross,
42
43 SkipOnInsufficientFundsAndFailOnCross,
46}
47
48impl FailedMultipleLimitOrderBehavior {
49 pub fn should_fail_on_cross(&self) -> bool {
50 matches!(
51 self,
52 FailedMultipleLimitOrderBehavior::FailOnInsufficientFundsAndFailOnCross
53 | FailedMultipleLimitOrderBehavior::SkipOnInsufficientFundsAndFailOnCross
54 )
55 }
56
57 pub fn should_skip_orders_with_insufficient_funds(&self) -> bool {
58 matches!(
59 self,
60 FailedMultipleLimitOrderBehavior::SkipOnInsufficientFundsAndAmendOnCross
61 | FailedMultipleLimitOrderBehavior::SkipOnInsufficientFundsAndFailOnCross
62 )
63 }
64}
65
66#[derive(BorshDeserialize, BorshSerialize, Debug)]
68pub struct MultipleOrderPacket {
69 pub bids: Vec<CondensedOrder>,
71 pub asks: Vec<CondensedOrder>,
72 pub client_order_id: Option<u128>,
73 pub failed_multiple_limit_order_behavior: FailedMultipleLimitOrderBehavior,
74}
75
76#[derive(BorshDeserialize, BorshSerialize, Debug, Clone)]
77pub struct CondensedOrder {
78 pub price_in_ticks: u64,
79 pub size_in_base_lots: u64,
80 pub last_valid_slot: Option<u64>,
81 pub last_valid_unix_timestamp_in_seconds: Option<u64>,
82}
83
84impl CondensedOrder {
85 pub fn new_default(price_in_ticks: u64, size_in_base_lots: u64) -> Self {
86 CondensedOrder {
87 price_in_ticks,
88 size_in_base_lots,
89 last_valid_slot: None,
90 last_valid_unix_timestamp_in_seconds: None,
91 }
92 }
93}
94
95impl MultipleOrderPacket {
96 pub fn new(
97 bids: Vec<CondensedOrder>,
98 asks: Vec<CondensedOrder>,
99 client_order_id: Option<u128>,
100 reject_post_only: bool,
101 ) -> Self {
102 MultipleOrderPacket {
103 bids,
104 asks,
105 client_order_id,
106 failed_multiple_limit_order_behavior: if reject_post_only {
107 FailedMultipleLimitOrderBehavior::FailOnInsufficientFundsAndFailOnCross
108 } else {
109 FailedMultipleLimitOrderBehavior::FailOnInsufficientFundsAndAmendOnCross
110 },
111 }
112 }
113
114 pub fn new_default(bids: Vec<CondensedOrder>, asks: Vec<CondensedOrder>) -> Self {
115 MultipleOrderPacket {
116 bids,
117 asks,
118 client_order_id: None,
119 failed_multiple_limit_order_behavior:
120 FailedMultipleLimitOrderBehavior::FailOnInsufficientFundsAndFailOnCross,
121 }
122 }
123
124 pub fn new_with_failure_behavior(
125 bids: Vec<CondensedOrder>,
126 asks: Vec<CondensedOrder>,
127 client_order_id: Option<u128>,
128 failed_multiple_limit_order_behavior: FailedMultipleLimitOrderBehavior,
129 ) -> Self {
130 MultipleOrderPacket {
131 bids,
132 asks,
133 client_order_id,
134 failed_multiple_limit_order_behavior,
135 }
136 }
137}
138
139pub(crate) fn process_swap<'a, 'info>(
141 _program_id: &Pubkey,
142 market_context: &PhoenixMarketContext<'a, 'info>,
143 accounts: &'a [AccountInfo<'info>],
144 data: &[u8],
145 record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
146) -> ProgramResult {
147 sol_log_compute_units();
148 let new_order_context = NewOrderContext::load_cross_only(market_context, accounts, false)?;
149 let mut order_packet = decode_order_packet(data).ok_or_else(|| {
150 phoenix_log!("Failed to decode order packet");
151 ProgramError::InvalidInstructionData
152 })?;
153 assert_with_msg(
154 new_order_context.seat_option.is_none(),
155 ProgramError::InvalidInstructionData,
156 "Too many accounts",
157 )?;
158 assert_with_msg(
159 order_packet.is_take_only(),
160 ProgramError::InvalidInstructionData,
161 "Order type must be IOC or FOK",
162 )?;
163 assert_with_msg(
164 !order_packet.no_deposit_or_withdrawal(),
165 ProgramError::InvalidInstructionData,
166 "Instruction does not allow using deposited funds",
167 )?;
168 let mut order_ids = vec![];
169 process_new_order(
170 new_order_context,
171 market_context,
172 &mut order_packet,
173 record_event_fn,
174 &mut order_ids,
175 )
176}
177
178pub(crate) fn process_swap_with_free_funds<'a, 'info>(
183 _program_id: &Pubkey,
184 market_context: &PhoenixMarketContext<'a, 'info>,
185 accounts: &'a [AccountInfo<'info>],
186 data: &[u8],
187 record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
188) -> ProgramResult {
189 let new_order_context = NewOrderContext::load_cross_only(market_context, accounts, true)?;
190 let mut order_packet = decode_order_packet(data).ok_or_else(|| {
191 phoenix_log!("Failed to decode order packet");
192 ProgramError::InvalidInstructionData
193 })?;
194 assert_with_msg(
195 new_order_context.seat_option.is_some(),
196 ProgramError::InvalidInstructionData,
197 "Missing seat for market maker",
198 )?;
199 assert_with_msg(
200 order_packet.is_take_only(),
201 ProgramError::InvalidInstructionData,
202 "Order type must be IOC or FOK",
203 )?;
204 assert_with_msg(
205 order_packet.no_deposit_or_withdrawal(),
206 ProgramError::InvalidInstructionData,
207 "Order must be set to use only deposited funds",
208 )?;
209 let mut order_ids = vec![];
210 process_new_order(
211 new_order_context,
212 market_context,
213 &mut order_packet,
214 record_event_fn,
215 &mut order_ids,
216 )
217}
218
219pub(crate) fn process_place_limit_order<'a, 'info>(
222 _program_id: &Pubkey,
223 market_context: &PhoenixMarketContext<'a, 'info>,
224 accounts: &'a [AccountInfo<'info>],
225 data: &[u8],
226 record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
227 order_ids: &mut Vec<FIFOOrderId>,
228) -> ProgramResult {
229 let new_order_context = NewOrderContext::load_post_allowed(market_context, accounts, false)?;
230 let mut order_packet = decode_order_packet(data).ok_or_else(|| {
231 phoenix_log!("Failed to decode order packet");
232 ProgramError::InvalidInstructionData
233 })?;
234 assert_with_msg(
235 new_order_context.seat_option.is_some(),
236 ProgramError::InvalidInstructionData,
237 "Missing seat for market maker",
238 )?;
239 assert_with_msg(
240 !order_packet.is_take_only(),
241 ProgramError::InvalidInstructionData,
242 "Order type must be Limit or PostOnly",
243 )?;
244 assert_with_msg(
245 !order_packet.no_deposit_or_withdrawal(),
246 ProgramError::InvalidInstructionData,
247 "Instruction does not allow using deposited funds",
248 )?;
249 process_new_order(
250 new_order_context,
251 market_context,
252 &mut order_packet,
253 record_event_fn,
254 order_ids,
255 )
256}
257
258pub(crate) fn process_place_limit_order_with_free_funds<'a, 'info>(
263 _program_id: &Pubkey,
264 market_context: &PhoenixMarketContext<'a, 'info>,
265 accounts: &'a [AccountInfo<'info>],
266 data: &[u8],
267 record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
268 order_ids: &mut Vec<FIFOOrderId>,
269) -> ProgramResult {
270 let new_order_context = NewOrderContext::load_post_allowed(market_context, accounts, true)?;
271 let mut order_packet = decode_order_packet(data).ok_or_else(|| {
272 phoenix_log!("Failed to decode order packet");
273 ProgramError::InvalidInstructionData
274 })?;
275 assert_with_msg(
276 new_order_context.seat_option.is_some(),
277 ProgramError::InvalidInstructionData,
278 "Missing seat for market maker",
279 )?;
280 assert_with_msg(
281 !order_packet.is_take_only(),
282 ProgramError::InvalidInstructionData,
283 "Order type must be Limit or PostOnly",
284 )?;
285 assert_with_msg(
286 order_packet.no_deposit_or_withdrawal(),
287 ProgramError::InvalidInstructionData,
288 "Order must be set to use only deposited funds",
289 )?;
290 process_new_order(
291 new_order_context,
292 market_context,
293 &mut order_packet,
294 record_event_fn,
295 order_ids,
296 )
297}
298
299pub(crate) fn process_place_multiple_post_only_orders<'a, 'info>(
304 _program_id: &Pubkey,
305 market_context: &PhoenixMarketContext<'a, 'info>,
306 accounts: &'a [AccountInfo<'info>],
307 data: &[u8],
308 record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
309 order_ids: &mut Vec<FIFOOrderId>,
310) -> ProgramResult {
311 let new_order_context = NewOrderContext::load_post_allowed(market_context, accounts, false)?;
312 let multiple_order_packet = MultipleOrderPacket::try_from_slice(data)?;
313 assert_with_msg(
314 new_order_context.seat_option.is_some(),
315 ProgramError::InvalidInstructionData,
316 "Missing seat for market maker",
317 )?;
318
319 process_multiple_new_orders(
320 new_order_context,
321 market_context,
322 multiple_order_packet,
323 record_event_fn,
324 order_ids,
325 false,
326 )
327}
328
329pub(crate) fn process_place_multiple_post_only_orders_with_free_funds<'a, 'info>(
336 _program_id: &Pubkey,
337 market_context: &PhoenixMarketContext<'a, 'info>,
338 accounts: &'a [AccountInfo<'info>],
339 data: &[u8],
340 record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
341 order_ids: &mut Vec<FIFOOrderId>,
342) -> ProgramResult {
343 let new_order_context = NewOrderContext::load_post_allowed(market_context, accounts, true)?;
344 let multiple_order_packet = MultipleOrderPacket::try_from_slice(data)?;
345 assert_with_msg(
346 new_order_context.seat_option.is_some(),
347 ProgramError::InvalidInstructionData,
348 "Missing seat for market maker",
349 )?;
350 process_multiple_new_orders(
351 new_order_context,
352 market_context,
353 multiple_order_packet,
354 record_event_fn,
355 order_ids,
356 true,
357 )
358}
359
360fn process_new_order<'a, 'info>(
361 new_order_context: NewOrderContext<'a, 'info>,
362 market_context: &PhoenixMarketContext<'a, 'info>,
363 order_packet: &mut OrderPacket,
364 record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
365 order_ids: &mut Vec<FIFOOrderId>,
366) -> ProgramResult {
367 let PhoenixMarketContext {
368 market_info,
369 signer: trader,
370 } = market_context;
371 let NewOrderContext { vault_context, .. } = new_order_context;
372 let (quote_lot_size, base_lot_size) = {
373 let header = market_info.get_header()?;
374 (header.get_quote_lot_size(), header.get_base_lot_size())
375 };
376
377 let side = order_packet.side();
378 let (
379 quote_atoms_to_withdraw,
380 quote_atoms_to_deposit,
381 base_atoms_to_withdraw,
382 base_atoms_to_deposit,
383 ) = {
384 let clock = Clock::get()?;
385 let mut get_clock_fn = || (clock.slot, clock.unix_timestamp as u64);
386 let market_bytes = &mut market_info.try_borrow_mut_data()?[size_of::<MarketHeader>()..];
387 let market_wrapper = load_with_dispatch_mut(&market_info.size_params, market_bytes)?;
388
389 if order_packet.fail_silently_on_insufficient_funds() {
392 let (base_lots_available, quote_lots_available) = get_available_balances_for_trader(
393 &market_wrapper,
394 trader.key,
395 vault_context.as_ref(),
396 base_lot_size,
397 quote_lot_size,
398 )?;
399 if !order_packet_has_sufficient_funds(
400 &market_wrapper,
401 order_packet,
402 base_lots_available,
403 quote_lots_available,
404 ) {
405 return Ok(());
406 }
407 }
408
409 let (order_id, matching_engine_response) = market_wrapper
410 .inner
411 .place_order(
412 trader.key,
413 *order_packet,
414 record_event_fn,
415 &mut get_clock_fn,
416 )
417 .ok_or(PhoenixError::NewOrderError)?;
418
419 if let Some(order_id) = order_id {
420 order_ids.push(order_id);
421 }
422
423 (
424 matching_engine_response.num_quote_lots_out * quote_lot_size,
425 matching_engine_response.get_deposit_amount_bid_in_quote_lots() * quote_lot_size,
426 matching_engine_response.num_base_lots_out * base_lot_size,
427 matching_engine_response.get_deposit_amount_ask_in_base_lots() * base_lot_size,
428 )
429 };
430 let header = market_info.get_header()?;
431 let quote_params = &header.quote_params;
432 let base_params = &header.base_params;
433
434 if quote_atoms_to_withdraw > QuoteAtoms::ZERO || base_atoms_to_withdraw > BaseAtoms::ZERO {
435 let status = MarketStatus::from(header.status);
436 assert_with_msg(
437 status.cross_allowed(),
438 ProgramError::InvalidAccountData,
439 &format!("Market is not active, market status is {}", status),
440 )?;
441 }
442 if !order_packet.no_deposit_or_withdrawal() {
443 if let Some(PhoenixVaultContext {
444 base_account,
445 quote_account,
446 base_vault,
447 quote_vault,
448 token_program,
449 }) = vault_context
450 {
451 match side {
452 Side::Bid => {
453 maybe_invoke_withdraw(
454 market_info.key,
455 &base_params.mint_key,
456 base_params.vault_bump as u8,
457 base_atoms_to_withdraw.as_u64(),
458 &token_program,
459 &base_account,
460 &base_vault,
461 )?;
462 maybe_invoke_deposit(
463 quote_atoms_to_deposit.as_u64(),
464 &token_program,
465 "e_account,
466 "e_vault,
467 trader.as_ref(),
468 )?;
469 }
470 Side::Ask => {
471 maybe_invoke_withdraw(
472 market_info.key,
473 "e_params.mint_key,
474 quote_params.vault_bump as u8,
475 quote_atoms_to_withdraw.as_u64(),
476 &token_program,
477 "e_account,
478 "e_vault,
479 )?;
480 maybe_invoke_deposit(
481 base_atoms_to_deposit.as_u64(),
482 &token_program,
483 &base_account,
484 &base_vault,
485 trader.as_ref(),
486 )?;
487 }
488 }
489 } else {
490 phoenix_log!("WARNING: Vault context was not provided");
492 return Err(PhoenixError::NewOrderError.into());
493 }
494 } else if quote_atoms_to_deposit > QuoteAtoms::ZERO || base_atoms_to_deposit > BaseAtoms::ZERO {
495 phoenix_log!("WARNING: Deposited amount of funds were insufficient to execute the order");
497 return Err(ProgramError::InsufficientFunds);
498 }
499
500 Ok(())
501}
502
503fn process_multiple_new_orders<'a, 'info>(
504 new_order_context: NewOrderContext<'a, 'info>,
505 market_context: &PhoenixMarketContext<'a, 'info>,
506 multiple_order_packet: MultipleOrderPacket,
507 record_event_fn: &mut dyn FnMut(MarketEvent<Pubkey>),
508 order_ids: &mut Vec<FIFOOrderId>,
509 no_deposit: bool,
510) -> ProgramResult {
511 let PhoenixMarketContext {
512 market_info,
513 signer: trader,
514 } = market_context;
515 let NewOrderContext { vault_context, .. } = new_order_context;
516
517 let MultipleOrderPacket {
518 bids,
519 asks,
520 client_order_id,
521 failed_multiple_limit_order_behavior,
522 } = multiple_order_packet;
523
524 let highest_bid = bids
525 .iter()
526 .map(|bid| bid.price_in_ticks)
527 .max_by(|bid1, bid2| bid1.cmp(&bid2))
528 .unwrap_or(0);
529
530 let lowest_ask = asks
531 .iter()
532 .map(|ask| ask.price_in_ticks)
533 .min_by(|ask1, ask2| ask1.cmp(&ask2))
534 .unwrap_or(u64::MAX);
535
536 if highest_bid >= lowest_ask {
537 phoenix_log!("Invalid input. MultipleOrderPacket contains crossing bids and asks");
538 return Err(ProgramError::InvalidArgument.into());
539 }
540
541 let client_order_id = client_order_id.unwrap_or(0);
542 let mut quote_lots_to_deposit = QuoteLots::ZERO;
543 let mut base_lots_to_deposit = BaseLots::ZERO;
544 let (quote_lot_size, base_lot_size) = {
545 let header = market_info.get_header()?;
546 (header.get_quote_lot_size(), header.get_base_lot_size())
547 };
548
549 {
550 let clock = Clock::get()?;
551 let mut get_clock_fn = || (clock.slot, clock.unix_timestamp as u64);
552 let market_bytes = &mut market_info.try_borrow_mut_data()?[size_of::<MarketHeader>()..];
553 let market_wrapper = load_with_dispatch_mut(&market_info.size_params, market_bytes)?;
554
555 let (mut base_lots_available, mut quote_lots_available) =
556 get_available_balances_for_trader(
557 &market_wrapper,
558 trader.key,
559 vault_context.as_ref(),
560 base_lot_size,
561 quote_lot_size,
562 )?;
563
564 for (book_orders, side) in [(&bids, Side::Bid), (&asks, Side::Ask)].iter() {
565 for CondensedOrder {
566 price_in_ticks,
567 size_in_base_lots,
568 last_valid_slot,
569 last_valid_unix_timestamp_in_seconds,
570 } in book_orders
571 .iter()
572 .sorted_by(|o1, o2| o1.price_in_ticks.cmp(&o2.price_in_ticks))
573 .group_by(|o| {
574 (
575 o.price_in_ticks,
576 o.last_valid_slot,
577 o.last_valid_unix_timestamp_in_seconds,
578 )
579 })
580 .into_iter()
581 .map(
582 |(
583 (price_in_ticks, last_valid_slot, last_valid_unix_timestamp_in_seconds),
584 level,
585 )| CondensedOrder {
586 price_in_ticks,
587 size_in_base_lots: level.fold(0, |acc, o| acc + o.size_in_base_lots),
588 last_valid_slot,
589 last_valid_unix_timestamp_in_seconds,
590 },
591 )
592 {
593 let order_packet = OrderPacket::PostOnly {
594 side: *side,
595 price_in_ticks: Ticks::new(price_in_ticks),
596 num_base_lots: BaseLots::new(size_in_base_lots),
597 client_order_id,
598 reject_post_only: failed_multiple_limit_order_behavior.should_fail_on_cross(),
599 use_only_deposited_funds: no_deposit,
600 last_valid_slot,
601 last_valid_unix_timestamp_in_seconds,
602 fail_silently_on_insufficient_funds: failed_multiple_limit_order_behavior
603 .should_skip_orders_with_insufficient_funds(),
604 };
605
606 let matching_engine_response = {
607 if failed_multiple_limit_order_behavior
608 .should_skip_orders_with_insufficient_funds()
609 && !order_packet_has_sufficient_funds(
610 &market_wrapper,
611 &order_packet,
612 base_lots_available,
613 quote_lots_available,
614 )
615 {
616 continue;
618 }
619 let (order_id, matching_engine_response) = market_wrapper
620 .inner
621 .place_order(trader.key, order_packet, record_event_fn, &mut get_clock_fn)
622 .ok_or(PhoenixError::NewOrderError)?;
623 if let Some(order_id) = order_id {
624 order_ids.push(order_id);
625 }
626 matching_engine_response
627 };
628
629 let quote_lots_deposited =
630 matching_engine_response.get_deposit_amount_bid_in_quote_lots();
631 let base_lots_deposited =
632 matching_engine_response.get_deposit_amount_ask_in_base_lots();
633
634 if failed_multiple_limit_order_behavior.should_skip_orders_with_insufficient_funds()
635 {
636 quote_lots_available -=
639 quote_lots_deposited + matching_engine_response.num_free_quote_lots_used;
640 base_lots_available -=
641 base_lots_deposited + matching_engine_response.num_free_base_lots_used;
642 }
643
644 quote_lots_to_deposit += quote_lots_deposited;
645 base_lots_to_deposit += base_lots_deposited;
646 }
647 }
648 }
649
650 if !no_deposit {
651 if let Some(PhoenixVaultContext {
652 base_account,
653 quote_account,
654 base_vault,
655 quote_vault,
656 token_program,
657 }) = vault_context
658 {
659 if !bids.is_empty() {
660 maybe_invoke_deposit(
661 (quote_lots_to_deposit * quote_lot_size).as_u64(),
662 &token_program,
663 "e_account,
664 "e_vault,
665 trader.as_ref(),
666 )?;
667 } else {
668 assert_with_msg(
669 quote_lots_to_deposit == QuoteLots::ZERO,
670 PhoenixError::NewOrderError,
671 "WARNING: Expected quote_lots_to_deposit to be zero",
672 )?;
673 }
674 if !asks.is_empty() {
675 maybe_invoke_deposit(
676 (base_lots_to_deposit * base_lot_size).as_u64(),
677 &token_program,
678 &base_account,
679 &base_vault,
680 trader.as_ref(),
681 )?;
682 } else {
683 assert_with_msg(
684 base_lots_to_deposit == BaseLots::ZERO,
685 PhoenixError::NewOrderError,
686 "WARNING: Expected base_lots_to_deposit to be zero",
687 )?;
688 }
689 } else {
690 phoenix_log!("WARNING: Vault context was not provided");
692 return Err(PhoenixError::NewOrderError.into());
693 }
694 } else if base_lots_to_deposit > BaseLots::ZERO || quote_lots_to_deposit > QuoteLots::ZERO {
695 phoenix_log!("Deposited amount of funds were insufficient to execute the order");
696 return Err(ProgramError::InsufficientFunds);
697 }
698
699 Ok(())
700}
701
702fn get_available_balances_for_trader<'a>(
703 market_wrapper: &MarketWrapperMut<'a, Pubkey, FIFOOrderId, FIFORestingOrder, OrderPacket>,
704 trader: &Pubkey,
705 vault_context: Option<&PhoenixVaultContext>,
706 base_lot_size: BaseAtomsPerBaseLot,
707 quote_lot_size: QuoteAtomsPerQuoteLot,
708) -> Result<(BaseLots, QuoteLots), ProgramError> {
709 let (base_lots_free, quote_lots_free) = {
710 let trader_index = market_wrapper
711 .inner
712 .get_trader_index(trader)
713 .ok_or(PhoenixError::TraderNotFound)?;
714 let trader_state = market_wrapper
715 .inner
716 .get_trader_state_from_index(trader_index);
717 (trader_state.base_lots_free, trader_state.quote_lots_free)
718 };
719 let (base_lots_available, quote_lots_available) = match vault_context.as_ref() {
720 None => (base_lots_free, quote_lots_free),
721 Some(ctx) => {
722 let quote_account_atoms = ctx.quote_account.amount().map(QuoteAtoms::new)?;
723 let base_account_atoms = ctx.base_account.amount().map(BaseAtoms::new)?;
724 (
725 base_lots_free + base_account_atoms.unchecked_div(base_lot_size),
726 quote_lots_free + quote_account_atoms.unchecked_div(quote_lot_size),
727 )
728 }
729 };
730 Ok((base_lots_available, quote_lots_available))
731}
732
733fn order_packet_has_sufficient_funds<'a>(
734 market_wrapper: &MarketWrapperMut<'a, Pubkey, FIFOOrderId, FIFORestingOrder, OrderPacket>,
735 order_packet: &OrderPacket,
736 base_lots_available: BaseLots,
737 quote_lots_available: QuoteLots,
738) -> bool {
739 match order_packet.side() {
740 Side::Ask => {
741 if base_lots_available < order_packet.num_base_lots() {
742 phoenix_log!(
743 "Insufficient funds to place order: {} base lots available, {} base lots required",
744 base_lots_available,
745 order_packet.num_base_lots()
746 );
747 return false;
748 }
749 }
750 Side::Bid => {
751 let quote_lots_required = order_packet.get_price_in_ticks()
752 * market_wrapper.inner.get_tick_size()
753 * order_packet.num_base_lots()
754 / market_wrapper.inner.get_base_lots_per_base_unit();
755
756 if quote_lots_available < quote_lots_required {
757 phoenix_log!(
758 "Insufficient funds to place order: {} quote lots available, {} quote lots required",
759 quote_lots_available,
760 quote_lots_required
761 );
762 return false;
763 }
764 }
765 }
766 true
767}