mpl_auction/
processor.rs

1use crate::errors::AuctionError;
2use arrayref::array_ref;
3use borsh::{BorshDeserialize, BorshSerialize};
4use solana_program::{
5    account_info::AccountInfo, borsh::try_from_slice_unchecked, clock::UnixTimestamp,
6    entrypoint::ProgramResult, hash::Hash, msg, program_error::ProgramError, pubkey::Pubkey,
7};
8use std::{cell::Ref, cmp, mem};
9
10// Declare submodules, each contains a single handler for each instruction variant in the program.
11pub mod cancel_bid;
12pub mod claim_bid;
13pub mod create_auction;
14pub mod create_auction_v2;
15pub mod end_auction;
16pub mod place_bid;
17pub mod set_authority;
18pub mod start_auction;
19
20// Re-export submodules handlers + associated types for other programs to consume.
21pub use cancel_bid::*;
22pub use claim_bid::*;
23pub use create_auction::*;
24pub use create_auction_v2::*;
25pub use end_auction::*;
26pub use place_bid::*;
27pub use set_authority::*;
28pub use start_auction::*;
29
30pub fn process_instruction(
31    program_id: &Pubkey,
32    accounts: &[AccountInfo],
33    input: &[u8],
34) -> ProgramResult {
35    use crate::instruction::AuctionInstruction;
36    match AuctionInstruction::try_from_slice(input)? {
37        AuctionInstruction::CancelBid(args) => cancel_bid(program_id, accounts, args),
38        AuctionInstruction::ClaimBid(args) => claim_bid(program_id, accounts, args),
39        AuctionInstruction::CreateAuction(args) => {
40            create_auction(program_id, accounts, args, None, None)
41        }
42        AuctionInstruction::CreateAuctionV2(args) => create_auction_v2(program_id, accounts, args),
43        AuctionInstruction::EndAuction(args) => end_auction(program_id, accounts, args),
44        AuctionInstruction::PlaceBid(args) => place_bid(program_id, accounts, args),
45        AuctionInstruction::SetAuthority => set_authority(program_id, accounts),
46        AuctionInstruction::StartAuction(args) => start_auction(program_id, accounts, args),
47    }
48}
49
50/// Structure with pricing floor data.
51#[repr(C)]
52#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
53pub enum PriceFloor {
54    /// Due to borsh on the front end disallowing different arguments in enums, we have to make sure data is
55    /// same size across all three
56    /// No price floor, any bid is valid.
57    None([u8; 32]),
58    /// Explicit minimum price, any bid below this is rejected.
59    MinimumPrice([u64; 4]),
60    /// Hidden minimum price, revealed at the end of the auction.
61    BlindedPrice(Hash),
62}
63
64// The two extra 8's are present, one 8 is for the Vec's amount of elements and one is for the max
65// usize in bid state.
66// NOTE: New research suggests u32s are used for vecs in borsh, not u64s, so the first extra 8 should be a 4
67// but for legacy reasons we leave it behind.
68pub const BASE_AUCTION_DATA_SIZE: usize = 32 + 32 + 9 + 9 + 9 + 9 + 1 + 32 + 1 + 8 + 8 + 8;
69pub const BID_LENGTH: usize = 32 + 8;
70#[repr(C)]
71#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
72pub struct AuctionData {
73    /// Pubkey of the authority with permission to modify this auction.
74    pub authority: Pubkey,
75    /// Pubkey of the resource being bid on.
76    /// TODO try to bring this back some day. Had to remove this due to a stack access violation bug
77    /// interactin that happens in metaplex during redemptions due to some low level rust error
78    /// that happens when AuctionData has too many fields. This field was the least used.
79    ///pub resource: Pubkey,
80    /// Token mint for the SPL token being used to bid
81    pub token_mint: Pubkey,
82    /// The time the last bid was placed, used to keep track of auction timing.
83    pub last_bid: Option<UnixTimestamp>,
84    /// Slot time the auction was officially ended by.
85    pub ended_at: Option<UnixTimestamp>,
86    /// End time is the cut-off point that the auction is forced to end by.
87    pub end_auction_at: Option<UnixTimestamp>,
88    /// Gap time is the amount of time in slots after the previous bid at which the auction ends.
89    pub end_auction_gap: Option<UnixTimestamp>,
90    /// Minimum price for any bid to meet.
91    pub price_floor: PriceFloor,
92    /// The state the auction is in, whether it has started or ended.
93    pub state: AuctionState,
94    /// Auction Bids, each user may have one bid open at a time.
95    pub bid_state: BidState,
96}
97
98// Alias for auction name.
99pub type AuctionName = [u8; 32];
100
101pub const MAX_AUCTION_DATA_EXTENDED_SIZE: usize = 8 + 9 + 2 + 9 + 33 + 158;
102// Further storage for more fields. Would like to store more on the main data but due
103// to a borsh issue that causes more added fields to inflict "Access violation" errors
104// during redemption in main Metaplex app for no reason, we had to add this nasty PDA.
105#[repr(C)]
106#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
107pub struct AuctionDataExtended {
108    /// Total uncancelled bids
109    pub total_uncancelled_bids: u64,
110    // Unimplemented fields
111    /// Tick size
112    pub tick_size: Option<u64>,
113    /// gap_tick_size_percentage - two decimal points
114    pub gap_tick_size_percentage: Option<u8>,
115    /// Instant sale price
116    pub instant_sale_price: Option<u64>,
117    /// Auction name
118    pub name: Option<AuctionName>,
119}
120
121impl AuctionDataExtended {
122    pub fn from_account_info(a: &AccountInfo) -> Result<AuctionDataExtended, ProgramError> {
123        if a.data_len() != MAX_AUCTION_DATA_EXTENDED_SIZE {
124            return Err(AuctionError::DataTypeMismatch.into());
125        }
126
127        let auction_extended: AuctionDataExtended = try_from_slice_unchecked(&a.data.borrow_mut())?;
128
129        Ok(auction_extended)
130    }
131
132    pub fn get_instant_sale_price<'a>(data: &'a Ref<'a, &'a mut [u8]>) -> Option<u64> {
133        if let Some(idx) = Self::find_instant_sale_beginning(data) {
134            Some(u64::from_le_bytes(*array_ref![data, idx, 8]))
135        } else {
136            None
137        }
138    }
139
140    fn find_instant_sale_beginning<'a>(data: &'a Ref<'a, &'a mut [u8]>) -> Option<usize> {
141        // total_uncancelled_bids + tick_size Option
142        let mut instant_sale_beginning = 8;
143
144        // gaps for tick_size and gap_tick_size_percentage
145        let gaps = [9, 2];
146
147        for gap in gaps.iter() {
148            if data[instant_sale_beginning] == 1 {
149                instant_sale_beginning += gap;
150            } else {
151                instant_sale_beginning += 1;
152            }
153        }
154
155        // check if instant_sale_price has some value
156        if data[instant_sale_beginning] == 1 {
157            Some(instant_sale_beginning + 1)
158        } else {
159            None
160        }
161    }
162}
163
164impl AuctionData {
165    // Cheap methods to get at AuctionData without supremely expensive borsh deserialization calls.
166
167    pub fn get_token_mint(a: &AccountInfo) -> Pubkey {
168        let data = a.data.borrow();
169        let token_mint_data = array_ref![data, 32, 32];
170        Pubkey::new_from_array(*token_mint_data)
171    }
172
173    pub fn get_state(a: &AccountInfo) -> Result<AuctionState, ProgramError> {
174        // Remove the +1 to get rid of first byte of first bid, then -4 to subtract the u32 that is vec size of bids,
175        // now we're back at the beginning of the u32, -1 again to get to state
176        let bid_state_beginning = AuctionData::find_bid_state_beginning(a) - 1 - 4 - 1;
177        match a.data.borrow()[bid_state_beginning] {
178            0 => Ok(AuctionState::Created),
179            1 => Ok(AuctionState::Started),
180            2 => Ok(AuctionState::Ended),
181            _ => Err(ProgramError::InvalidAccountData),
182        }
183    }
184
185    pub fn get_num_winners(a: &AccountInfo) -> usize {
186        let (bid_state_beginning, num_elements, max) = AuctionData::get_vec_info(a);
187        std::cmp::min(num_elements, max)
188    }
189
190    fn find_bid_state_beginning(a: &AccountInfo) -> usize {
191        let data = a.data.borrow();
192        let mut bid_state_beginning = 32 + 32;
193
194        for i in 0..4 {
195            // One for each unix timestamp
196            if data[bid_state_beginning] == 1 {
197                bid_state_beginning += 9
198            } else {
199                bid_state_beginning += 1;
200            }
201        }
202
203        // Finally add price floor (enum + hash) and state, then the u32,
204        // then add 1 to position at the beginning of first bid.
205        bid_state_beginning += 1 + 32 + 1 + 4 + 1;
206        return bid_state_beginning;
207    }
208
209    fn get_vec_info(a: &AccountInfo) -> (usize, usize, usize) {
210        let bid_state_beginning = AuctionData::find_bid_state_beginning(a);
211        let data = a.data.borrow();
212
213        let num_elements_data = array_ref![data, bid_state_beginning - 4, 4];
214        let num_elements = u32::from_le_bytes(*num_elements_data) as usize;
215        let max_data = array_ref![data, bid_state_beginning + BID_LENGTH * num_elements, 8];
216        let max = u64::from_le_bytes(*max_data) as usize;
217
218        (bid_state_beginning, num_elements, max)
219    }
220
221    pub fn get_is_winner(a: &AccountInfo, key: &Pubkey) -> Option<usize> {
222        let bid_state_beginning = AuctionData::find_bid_state_beginning(a);
223        let data = a.data.borrow();
224        let as_bytes = key.to_bytes();
225        let (bid_state_beginning, num_elements, max) = AuctionData::get_vec_info(a);
226        for idx in 0..std::cmp::min(num_elements, max) {
227            match AuctionData::get_winner_at_inner(
228                &a.data.borrow(),
229                idx,
230                bid_state_beginning,
231                num_elements,
232                max,
233            ) {
234                Some(bid_key) => {
235                    // why deserialize the entire key to compare the two with a short circuit comparison
236                    // when we can compare them immediately?
237                    let mut matching = true;
238                    for bid_key_idx in 0..32 {
239                        if bid_key[bid_key_idx] != as_bytes[bid_key_idx] {
240                            matching = false;
241                            break;
242                        }
243                    }
244                    if matching {
245                        return Some(idx as usize);
246                    }
247                }
248                None => return None,
249            }
250        }
251        None
252    }
253
254    pub fn get_winner_at(a: &AccountInfo, idx: usize) -> Option<Pubkey> {
255        let (bid_state_beginning, num_elements, max) = AuctionData::get_vec_info(a);
256        match AuctionData::get_winner_at_inner(
257            &a.data.borrow(),
258            idx,
259            bid_state_beginning,
260            num_elements,
261            max,
262        ) {
263            Some(bid_key) => Some(Pubkey::new_from_array(*bid_key)),
264            None => None,
265        }
266    }
267
268    fn get_winner_at_inner<'a>(
269        data: &'a Ref<'a, &'a mut [u8]>,
270        idx: usize,
271        bid_state_beginning: usize,
272        num_elements: usize,
273        max: usize,
274    ) -> Option<&'a [u8; 32]> {
275        if idx + 1 > num_elements || idx + 1 > max {
276            return None;
277        }
278        Some(array_ref![
279            data,
280            bid_state_beginning + (num_elements - idx - 1) * BID_LENGTH,
281            32
282        ])
283    }
284
285    pub fn get_winner_bid_amount_at(a: &AccountInfo, idx: usize) -> Option<u64> {
286        let (bid_state_beginning, num_elements, max) = AuctionData::get_vec_info(a);
287        match AuctionData::get_winner_bid_amount_at_inner(
288            &a.data.borrow(),
289            idx,
290            bid_state_beginning,
291            num_elements,
292            max,
293        ) {
294            Some(bid_amount) => Some(bid_amount),
295            None => None,
296        }
297    }
298
299    fn get_winner_bid_amount_at_inner<'a>(
300        data: &'a Ref<'a, &'a mut [u8]>,
301        idx: usize,
302        bid_state_beginning: usize,
303        num_elements: usize,
304        max: usize,
305    ) -> Option<u64> {
306        if idx + 1 > num_elements || idx + 1 > max {
307            return None;
308        }
309        Some(u64::from_le_bytes(*array_ref![
310            data,
311            bid_state_beginning + (num_elements - idx - 1) * BID_LENGTH + 32,
312            8
313        ]))
314    }
315
316    pub fn from_account_info(a: &AccountInfo) -> Result<AuctionData, ProgramError> {
317        if (a.data_len() - BASE_AUCTION_DATA_SIZE) % mem::size_of::<Bid>() != 0 {
318            return Err(AuctionError::DataTypeMismatch.into());
319        }
320
321        let auction: AuctionData = try_from_slice_unchecked(&a.data.borrow_mut())?;
322
323        Ok(auction)
324    }
325
326    pub fn ended(&self, now: UnixTimestamp) -> Result<bool, ProgramError> {
327        // If there is an end time specified, handle conditions.
328        return match (self.ended_at, self.end_auction_gap) {
329            // NOTE if changing this, change in auction.ts on front end as well where logic duplicates.
330            // Both end and gap present, means a bid can still be placed post-auction if it is
331            // within the gap time.
332            (Some(end), Some(gap)) => {
333                // Check if the bid is within the gap between the last bidder.
334                if let Some(last) = self.last_bid {
335                    let next_bid_time = last
336                        .checked_add(gap)
337                        .ok_or(AuctionError::NumericalOverflowError)?;
338
339                    Ok(now > end && now > next_bid_time)
340                } else {
341                    Ok(now > end)
342                }
343            }
344
345            // Simply whether now has passed the end.
346            (Some(end), None) => Ok(now > end),
347
348            // No other end conditions.
349            _ => Ok(false),
350        };
351    }
352
353    pub fn is_winner(&self, key: &Pubkey) -> Option<usize> {
354        let minimum = match self.price_floor {
355            PriceFloor::MinimumPrice(min) => min[0],
356            _ => 0,
357        };
358        self.bid_state.is_winner(key, minimum)
359    }
360
361    pub fn num_winners(&self) -> u64 {
362        self.bid_state.num_winners()
363    }
364
365    pub fn num_possible_winners(&self) -> u64 {
366        self.bid_state.num_possible_winners()
367    }
368
369    pub fn winner_at(&self, idx: usize) -> Option<Pubkey> {
370        self.bid_state.winner_at(idx)
371    }
372
373    pub fn consider_instant_bid(&mut self, instant_sale_price: Option<u64>) {
374        // Check if all the lots were sold with instant_sale_price
375        if let Some(price) = instant_sale_price {
376            if self
377                .bid_state
378                .lowest_winning_bid_is_instant_bid_price(price)
379            {
380                msg!("All the lots were sold with instant_sale_price, auction is ended");
381                self.state = AuctionState::Ended;
382            }
383        }
384    }
385
386    pub fn place_bid(
387        &mut self,
388        bid: Bid,
389        tick_size: Option<u64>,
390        gap_tick_size_percentage: Option<u8>,
391        now: UnixTimestamp,
392        instant_sale_price: Option<u64>,
393    ) -> Result<(), ProgramError> {
394        let gap_val = match self.ended_at {
395            Some(end) => {
396                // We use the actual gap tick size perc if we're in gap window,
397                // otherwise we pass in none so the logic isnt used
398                if now > end {
399                    gap_tick_size_percentage
400                } else {
401                    None
402                }
403            }
404            None => None,
405        };
406        let minimum = match self.price_floor {
407            PriceFloor::MinimumPrice(min) => min[0],
408            _ => 0,
409        };
410
411        self.bid_state.place_bid(
412            bid,
413            tick_size,
414            gap_val,
415            minimum,
416            instant_sale_price,
417            &mut self.state,
418        )?;
419
420        self.consider_instant_bid(instant_sale_price);
421
422        Ok(())
423    }
424}
425
426/// Define valid auction state transitions.
427#[repr(C)]
428#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
429pub enum AuctionState {
430    Created,
431    Started,
432    Ended,
433}
434
435impl AuctionState {
436    pub fn create() -> Self {
437        AuctionState::Created
438    }
439
440    #[inline(always)]
441    pub fn start(self) -> Result<Self, ProgramError> {
442        match self {
443            AuctionState::Created => Ok(AuctionState::Started),
444            _ => Err(AuctionError::AuctionTransitionInvalid.into()),
445        }
446    }
447
448    #[inline(always)]
449    pub fn end(self) -> Result<Self, ProgramError> {
450        match self {
451            AuctionState::Started => Ok(AuctionState::Ended),
452            AuctionState::Created => Ok(AuctionState::Ended),
453            _ => Err(AuctionError::AuctionTransitionInvalid.into()),
454        }
455    }
456}
457
458/// Bids associate a bidding key with an amount bid.
459#[repr(C)]
460#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
461pub struct Bid(pub Pubkey, pub u64);
462
463/// BidState tracks the running state of an auction, each variant represents a different kind of
464/// auction being run.
465#[repr(C)]
466#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
467pub enum BidState {
468    EnglishAuction { bids: Vec<Bid>, max: usize },
469    OpenEdition { bids: Vec<Bid>, max: usize },
470}
471
472/// Bidding Implementations.
473///
474/// English Auction: this stores only the current winning bids in the auction, pruning cancelled
475/// and lost bids over time.
476///
477/// Open Edition: All bids are accepted, cancellations return money to the bidder and always
478/// succeed.
479impl BidState {
480    pub fn new_english(n: usize) -> Self {
481        BidState::EnglishAuction {
482            bids: vec![],
483            max: n,
484        }
485    }
486
487    pub fn new_open_edition() -> Self {
488        BidState::OpenEdition {
489            bids: vec![],
490            max: 0,
491        }
492    }
493
494    pub fn max_array_size_for(n: usize) -> usize {
495        let mut real_max = n;
496        if real_max < 8 {
497            real_max = 8;
498        } else {
499            real_max = 2 * real_max
500        }
501        real_max
502    }
503
504    fn assert_valid_tick_size_bid(bid: &Bid, tick_size: Option<u64>) -> ProgramResult {
505        if let Some(tick) = tick_size {
506            if bid.1.checked_rem(tick) != Some(0) {
507                msg!(
508                    "This bid {:?} is not a multiple of tick size {:?}, throw it out.",
509                    bid.1,
510                    tick_size
511                );
512                return Err(AuctionError::BidMustBeMultipleOfTickSize.into());
513            }
514        } else {
515            msg!("No tick size on this auction")
516        }
517
518        Ok(())
519    }
520
521    fn assert_valid_gap_insertion(
522        gap_tick: u8,
523        beaten_bid: &Bid,
524        beating_bid: &Bid,
525    ) -> ProgramResult {
526        // Use u128 to avoid potential overflow due to temporary mult of 100x since
527        // we haven't divided yet.
528        let mut minimum_bid_amount: u128 = (beaten_bid.1 as u128)
529            .checked_mul((100 + gap_tick) as u128)
530            .ok_or(AuctionError::NumericalOverflowError)?;
531        minimum_bid_amount = minimum_bid_amount
532            .checked_div(100u128)
533            .ok_or(AuctionError::NumericalOverflowError)?;
534
535        if minimum_bid_amount > beating_bid.1 as u128 {
536            msg!("Rejecting inserting this bid due to gap tick size of {:?} which causes min bid of {:?} from {:?} which is the bid it is trying to beat", gap_tick, minimum_bid_amount.to_string(), beaten_bid.1);
537            return Err(AuctionError::GapBetweenBidsTooSmall.into());
538        }
539
540        Ok(())
541    }
542
543    /// Push a new bid into the state, this succeeds only if the bid is larger than the current top
544    /// winner stored. Crappy list information to start with.
545    pub fn place_bid(
546        &mut self,
547        bid: Bid,
548        tick_size: Option<u64>,
549        gap_tick_size_percentage: Option<u8>,
550        minimum: u64,
551        instant_sale_price: Option<u64>,
552        auction_state: &mut AuctionState,
553    ) -> Result<(), ProgramError> {
554        msg!("Placing bid {:?}", &bid.1.to_string());
555        BidState::assert_valid_tick_size_bid(&bid, tick_size)?;
556        if bid.1 < minimum {
557            return Err(AuctionError::BidTooSmall.into());
558        }
559
560        match self {
561            // In a capped auction, track the limited number of winners.
562            BidState::EnglishAuction { ref mut bids, max } => {
563                match bids.last() {
564                    Some(top) => {
565                        msg!("Looking to go over the loop, but check tick size first");
566
567                        for i in (0..bids.len()).rev() {
568                            msg!("Comparison of {:?} and {:?} for {:?}", bids[i].1, bid.1, i);
569                            if bids[i].1 < bid.1 {
570                                if let Some(gap_tick) = gap_tick_size_percentage {
571                                    BidState::assert_valid_gap_insertion(gap_tick, &bids[i], &bid)?
572                                }
573
574                                msg!("Ok we can do an insert");
575                                if i + 1 < bids.len() {
576                                    msg!("Doing a normal insert");
577                                    bids.insert(i + 1, bid);
578                                } else {
579                                    msg!("Doing an on the end insert");
580                                    bids.push(bid)
581                                }
582                                break;
583                            } else if bids[i].1 == bid.1 {
584                                if let Some(gap_tick) = gap_tick_size_percentage {
585                                    if gap_tick > 0 {
586                                        msg!("Rejecting same-bid insert due to gap tick size of {:?}", gap_tick);
587                                        return Err(AuctionError::GapBetweenBidsTooSmall.into());
588                                    }
589                                }
590
591                                msg!("Ok we can do an equivalent insert");
592                                if i == 0 {
593                                    msg!("Doing a normal insert");
594                                    bids.insert(0, bid);
595                                    break;
596                                } else {
597                                    if bids[i - 1].1 != bids[i].1 {
598                                        msg!("Doing an insert just before");
599                                        bids.insert(i, bid);
600                                        break;
601                                    }
602                                    msg!("More duplicates ahead...")
603                                }
604                            } else if i == 0 {
605                                msg!("Inserting at 0");
606                                bids.insert(0, bid);
607                                break;
608                            }
609                        }
610
611                        let max_size = BidState::max_array_size_for(*max);
612
613                        if bids.len() > max_size {
614                            bids.remove(0);
615                        }
616                        Ok(())
617                    }
618                    _ => {
619                        msg!("Pushing bid onto stack");
620                        bids.push(bid);
621                        Ok(())
622                    }
623                }
624            }
625
626            // In an open auction, bidding simply succeeds.
627            BidState::OpenEdition { bids, max } => Ok(()),
628        }
629    }
630
631    /// Cancels a bid, if the bid was a winning bid it is removed, if the bid is invalid the
632    /// function simple no-ops.
633    pub fn cancel_bid(&mut self, key: Pubkey) -> Result<(), ProgramError> {
634        match self {
635            BidState::EnglishAuction { ref mut bids, max } => {
636                bids.retain(|b| b.0 != key);
637                Ok(())
638            }
639
640            // In an open auction, cancelling simply succeeds. It's up to the manager of an auction
641            // to decide what to do with open edition bids.
642            BidState::OpenEdition { bids, max } => Ok(()),
643        }
644    }
645
646    pub fn amount(&self, index: usize) -> u64 {
647        match self {
648            BidState::EnglishAuction { bids, max } => {
649                if index >= 0 as usize && index < bids.len() {
650                    return bids[bids.len() - index - 1].1;
651                } else {
652                    return 0;
653                }
654            }
655            BidState::OpenEdition { bids, max } => 0,
656        }
657    }
658
659    /// Check if a pubkey is currently a winner and return winner #1 as index 0 to outside world.
660    pub fn is_winner(&self, key: &Pubkey, min: u64) -> Option<usize> {
661        // NOTE if changing this, change in auction.ts on front end as well where logic duplicates.
662
663        match self {
664            // Presense in the winner list is enough to check win state.
665            BidState::EnglishAuction { bids, max } => {
666                match bids.iter().position(|bid| &bid.0 == key && bid.1 >= min) {
667                    Some(val) => {
668                        let zero_based_index = bids.len() - val - 1;
669                        if zero_based_index < *max {
670                            Some(zero_based_index)
671                        } else {
672                            None
673                        }
674                    }
675                    None => None,
676                }
677            }
678            // There are no winners in an open edition, it is up to the auction manager to decide
679            // what to do with open edition bids.
680            BidState::OpenEdition { bids, max } => None,
681        }
682    }
683
684    pub fn num_winners(&self) -> u64 {
685        match self {
686            BidState::EnglishAuction { bids, max } => cmp::min(bids.len(), *max) as u64,
687            BidState::OpenEdition { bids, max } => 0,
688        }
689    }
690
691    pub fn num_possible_winners(&self) -> u64 {
692        match self {
693            BidState::EnglishAuction { bids, max } => *max as u64,
694            BidState::OpenEdition { bids, max } => 0,
695        }
696    }
697
698    /// Idea is to present #1 winner as index 0 to outside world with this method
699    pub fn winner_at(&self, index: usize) -> Option<Pubkey> {
700        match self {
701            BidState::EnglishAuction { bids, max } => {
702                if index < *max && index < bids.len() {
703                    let bid = &bids[bids.len() - index - 1];
704                    Some(bids[bids.len() - index - 1].0)
705                } else {
706                    None
707                }
708            }
709            BidState::OpenEdition { bids, max } => None,
710        }
711    }
712
713    pub fn lowest_winning_bid_is_instant_bid_price(&self, instant_sale_amount: u64) -> bool {
714        match self {
715            // In a capped auction, track the limited number of winners.
716            BidState::EnglishAuction { bids, max } => {
717                // bids.len() - max = index of the last winner bid
718                bids.len() >= *max && bids[bids.len() - *max].1 >= instant_sale_amount
719            }
720            _ => false,
721        }
722    }
723}
724
725#[repr(C)]
726#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
727pub enum WinnerLimit {
728    Unlimited(usize),
729    Capped(usize),
730}
731
732pub const BIDDER_METADATA_LEN: usize = 32 + 32 + 8 + 8 + 1;
733/// Models a set of metadata for a bidder, meant to be stored in a PDA. This allows looking up
734/// information about a bidder regardless of if they have won, lost or cancelled.
735#[repr(C)]
736#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq, Debug)]
737pub struct BidderMetadata {
738    // Relationship with the bidder who's metadata this covers.
739    pub bidder_pubkey: Pubkey,
740    // Relationship with the auction this bid was placed on.
741    pub auction_pubkey: Pubkey,
742    // Amount that the user bid.
743    pub last_bid: u64,
744    // Tracks the last time this user bid.
745    pub last_bid_timestamp: UnixTimestamp,
746    // Whether the last bid the user made was cancelled. This should also be enough to know if the
747    // user is a winner, as if cancelled it implies previous bids were also cancelled.
748    pub cancelled: bool,
749}
750
751impl BidderMetadata {
752    pub fn from_account_info(a: &AccountInfo) -> Result<BidderMetadata, ProgramError> {
753        if a.data_len() != BIDDER_METADATA_LEN {
754            return Err(AuctionError::DataTypeMismatch.into());
755        }
756
757        let bidder_meta: BidderMetadata = try_from_slice_unchecked(&a.data.borrow_mut())?;
758
759        Ok(bidder_meta)
760    }
761}
762
763#[repr(C)]
764#[derive(Clone, BorshSerialize, BorshDeserialize, PartialEq)]
765pub struct BidderPot {
766    /// Points at actual pot that is a token account
767    pub bidder_pot: Pubkey,
768    /// Originating bidder account
769    pub bidder_act: Pubkey,
770    /// Auction account
771    pub auction_act: Pubkey,
772    /// emptied or not
773    pub emptied: bool,
774}
775
776impl BidderPot {
777    pub fn from_account_info(a: &AccountInfo) -> Result<BidderPot, ProgramError> {
778        if a.data_len() != mem::size_of::<BidderPot>() {
779            return Err(AuctionError::DataTypeMismatch.into());
780        }
781
782        let bidder_pot: BidderPot = try_from_slice_unchecked(&a.data.borrow_mut())?;
783
784        Ok(bidder_pot)
785    }
786}