From 377f6cd521e858c07c4192ca2cf1cc61418bfb3b Mon Sep 17 00:00:00 2001 From: Jordan Prince Date: Tue, 15 Jun 2021 11:31:51 -0500 Subject: [PATCH] Minimum Price Support in UI + Minor contract fix (#37) * Hook up minimum price to the front end with some borsh hacks, and fix a minor bug in the auction contract for minimum price checks * Remove excess definitions we dont need from the hack. * feat: no hacks allowed Co-authored-by: bartosz-lipinski <264380+bartosz-lipinski@users.noreply.github.com> --- js/packages/common/src/actions/auction.ts | 27 ++-- .../web/src/actions/createAuctionManager.ts | 3 + js/packages/web/src/actions/makeAuction.ts | 4 +- .../web/src/components/AuctionCard/index.tsx | 15 ++- .../src/components/AuctionNumbers/index.tsx | 98 +++++++------- .../web/src/views/auctionCreate/index.tsx | 120 ++++++++++++++---- .../program/src/processor/place_bid.rs | 2 +- 7 files changed, 184 insertions(+), 85 deletions(-) diff --git a/js/packages/common/src/actions/auction.ts b/js/packages/common/src/actions/auction.ts index 85eab0f4b11..72ebfffd4b6 100644 --- a/js/packages/common/src/actions/auction.ts +++ b/js/packages/common/src/actions/auction.ts @@ -117,13 +117,24 @@ export enum PriceFloorType { } export class PriceFloor { type: PriceFloorType; - // It's an array of 32 u8s, when minimum, only first 4 are used (a u64), when blinded price, the entire + // It's an array of 32 u8s, when minimum, only first 8 are used (a u64), when blinded price, the entire // thing is a hash and not actually a public key, and none is all zeroes - hash: PublicKey; + hash: Uint8Array; - constructor(args: { type: PriceFloorType; hash: PublicKey }) { + minPrice?: BN; + + constructor(args: { + type: PriceFloorType; + hash?: Uint8Array; + minPrice?: BN; + }) { this.type = args.type; - this.hash = args.hash; + this.hash = args.hash || new Uint8Array(32); + if (this.type === PriceFloorType.Minimum) { + if (args.minPrice) { + this.hash.set(args.minPrice.toArrayLike(Buffer, 'le', 8), 0); + } + } } } @@ -463,7 +474,7 @@ export const AUCTION_SCHEMA = new Map([ kind: 'struct', fields: [ ['type', 'u8'], - ['hash', 'pubkey'], + ['hash', [32]], ], }, ], @@ -528,6 +539,7 @@ export async function createAuction( resource: PublicKey, endAuctionAt: BN | null, auctionGap: BN | null, + priceFloor: PriceFloor, tokenMint: PublicKey, authority: PublicKey, creator: PublicKey, @@ -545,10 +557,7 @@ export async function createAuction( auctionGap, tokenMint, authority, - priceFloor: new PriceFloor({ - type: PriceFloorType.None, - hash: SystemProgram.programId, - }), + priceFloor, }), ), ); diff --git a/js/packages/web/src/actions/createAuctionManager.ts b/js/packages/web/src/actions/createAuctionManager.ts index a2b644bfd84..2e61f8b6532 100644 --- a/js/packages/web/src/actions/createAuctionManager.ts +++ b/js/packages/web/src/actions/createAuctionManager.ts @@ -20,6 +20,7 @@ import { getSafetyDepositBoxAddress, createAssociatedTokenAccountInstruction, sendTransactionWithRetry, + PriceFloor, } from '@oyster/common'; import { AccountLayout, Token } from '@solana/spl-token'; @@ -104,6 +105,7 @@ export async function createAuctionManager( safetyDepositDrafts: SafetyDepositDraft[], participationSafetyDepositDraft: SafetyDepositDraft | undefined, paymentMint: PublicKey, + priceFloor: PriceFloor, ): Promise<{ vault: PublicKey; auction: PublicKey; @@ -140,6 +142,7 @@ export async function createAuctionManager( endAuctionAt, auctionGap, paymentMint, + priceFloor, ); let safetyDepositConfigsWithPotentiallyUnsetTokens = diff --git a/js/packages/web/src/actions/makeAuction.ts b/js/packages/web/src/actions/makeAuction.ts index 7a146d6c3c0..d5de5178b43 100644 --- a/js/packages/web/src/actions/makeAuction.ts +++ b/js/packages/web/src/actions/makeAuction.ts @@ -1,5 +1,5 @@ import { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js'; -import { utils, actions, WinnerLimit } from '@oyster/common'; +import { utils, actions, WinnerLimit, PriceFloor } from '@oyster/common'; import BN from 'bn.js'; import { METAPLEX_PREFIX } from '../models/metaplex'; @@ -13,6 +13,7 @@ export async function makeAuction( endAuctionAt: BN, auctionGap: BN, paymentMint: PublicKey, + priceFloor: PriceFloor, ): Promise<{ auction: PublicKey; instructions: TransactionInstruction[]; @@ -45,6 +46,7 @@ export async function makeAuction( vault, endAuctionAt, auctionGap, + priceFloor, paymentMint, auctionManagerKey, wallet.publicKey, diff --git a/js/packages/web/src/components/AuctionCard/index.tsx b/js/packages/web/src/components/AuctionCard/index.tsx index 0facd1afda6..ea04cb7bbda 100644 --- a/js/packages/web/src/components/AuctionCard/index.tsx +++ b/js/packages/web/src/components/AuctionCard/index.tsx @@ -11,12 +11,10 @@ import { MetaplexOverlay, formatAmount, formatTokenAmount, - useMint + useMint, + PriceFloorType, } from '@oyster/common'; -import { - AuctionView, - useUserBalance, -} from '../../hooks'; +import { AuctionView, useUserBalance } from '../../hooks'; import { sendPlaceBid } from '../../actions/sendPlaceBid'; import { AuctionNumbers } from './../AuctionNumbers'; import { @@ -27,6 +25,7 @@ import { sendCancelBid } from '../../actions/cancelBid'; import BN from 'bn.js'; import { Confetti } from '../Confetti'; import { QUOTE_MINT } from '../../constants'; +import { LAMPORTS_PER_SOL } from '@solana/web3.js'; const { useWallet } = contexts.Wallet; @@ -67,7 +66,10 @@ export const AuctionCard = ({ winnerIndex = auctionView.auction.info.bidState.getWinnerIndex( auctionView.myBidderPot?.info.bidderAct, ); - + const priceFloor = + auctionView.auction.info.priceFloor.type == PriceFloorType.Minimum + ? auctionView.auction.info.priceFloor.minPrice?.toNumber() || 0 + : 0; const eligibleForOpenEdition = eligibleForParticipationPrizeGivenWinningIndex( winnerIndex, auctionView, @@ -331,6 +333,7 @@ export const AuctionCard = ({ disabled={ !myPayingAccount || value === undefined || + value * LAMPORTS_PER_SOL < priceFloor || loading || !accountByMint.get(QUOTE_MINT.toBase58()) } diff --git a/js/packages/web/src/components/AuctionNumbers/index.tsx b/js/packages/web/src/components/AuctionNumbers/index.tsx index 08912c631fc..ce59233f771 100644 --- a/js/packages/web/src/components/AuctionNumbers/index.tsx +++ b/js/packages/web/src/components/AuctionNumbers/index.tsx @@ -7,12 +7,9 @@ import { useMint, fromLamports, CountdownState, + PriceFloorType, } from '@oyster/common'; -import { - AuctionView, - AuctionViewState, - useBidsForAuction, -} from '../../hooks'; +import { AuctionView, AuctionViewState, useBidsForAuction } from '../../hooks'; import { AmountLabel } from '../AmountLabel'; export const AuctionNumbers = (props: { auctionView: AuctionView }) => { @@ -23,6 +20,12 @@ export const AuctionNumbers = (props: { auctionView: AuctionView }) => { const participationFixedPrice = auctionView.auctionManager.info.settings.participationConfig?.fixedPrice || 0; + const participationOnly = + auctionView.auctionManager.info.settings.winningConfigs.length == 0; + const priceFloor = + auctionView.auction.info.priceFloor.type == PriceFloorType.Minimum + ? auctionView.auction.info.priceFloor.minPrice?.toNumber() || 0 + : 0; const isUpcoming = auctionView.state === AuctionViewState.Upcoming; const isStarted = auctionView.state === AuctionViewState.Live; @@ -56,7 +59,10 @@ export const AuctionNumbers = (props: { auctionView: AuctionView }) => { style={{ marginBottom: 10 }} containerStyle={{ flexDirection: 'column' }} title="Starting bid" - amount={fromLamports(participationFixedPrice, mintInfo)} + amount={fromLamports( + participationOnly ? participationFixedPrice : priceFloor, + mintInfo, + )} /> )} {isStarted && bids.length > 0 && ( @@ -98,56 +104,62 @@ const Countdown = ({ state }: { state?: CountdownState }) => { > Time left - {state && (isEnded(state) ? ( - -
This auction has ended
-
- ) : ( - - {state && state.days > 0 && ( + {state && + (isEnded(state) ? ( + +
This auction has ended
+
+ ) : ( + + {state && state.days > 0 && ( + +
+ {state.days < 10 && ( + 0 + )} + {state.days} + : +
+
days
+ + )}
- {state.days < 10 && 0} - {state.days} + {state.hours < 10 && ( + 0 + )} + {state.hours} :
-
days
+
hour
- )} - -
- {state.hours < 10 && 0} - {state.hours} - : -
-
hour
- - -
- {state.minutes < 10 && ( - 0 - )} - {state.minutes} - {state.days === 0 && :} -
-
mins
- - {!state.days && (
- {state.seconds < 10 && ( + {state.minutes < 10 && ( 0 )} - {state.seconds} + {state.minutes} + {state.days === 0 && ( + : + )}
-
secs
+
mins
- )} -
- ))} + {!state.days && ( + +
+ {state.seconds < 10 && ( + 0 + )} + {state.seconds} +
+
secs
+ + )} +
+ ))} ); }; - diff --git a/js/packages/web/src/views/auctionCreate/index.tsx b/js/packages/web/src/views/auctionCreate/index.tsx index 32adf72751d..76a637527dc 100644 --- a/js/packages/web/src/views/auctionCreate/index.tsx +++ b/js/packages/web/src/views/auctionCreate/index.tsx @@ -28,8 +28,15 @@ import { toLamports, useMint, Creator, + PriceFloor, + PriceFloorType, } from '@oyster/common'; -import { Connection, PublicKey } from '@solana/web3.js'; +import { + Connection, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, +} from '@solana/web3.js'; import { MintLayout } from '@solana/spl-token'; import { useHistory, useParams } from 'react-router-dom'; import { capitalize } from 'lodash'; @@ -82,6 +89,7 @@ export interface AuctionState { // listed NFTs items: SafetyDepositDraft[]; participationNFT?: SafetyDepositDraft; + participationFixedPrice?: number; // number of editions for this auction (only applicable to limited edition) editions?: number; @@ -168,7 +176,9 @@ export const AuctionCreateView = () => { safetyDepositBoxIndex: 0, winnerConstraint: WinningConstraint.ParticipationPrizeGiven, nonWinningConstraint: NonWinningConstraint.GivenForFixedPrice, - fixedPrice: new BN(toLamports(attributes.priceFloor, mint) || 0), + fixedPrice: new BN( + toLamports(attributes.participationFixedPrice, mint) || 0, + ), }), }); @@ -223,7 +233,9 @@ export const AuctionCreateView = () => { safetyDepositBoxIndex: attributes.items.length, winnerConstraint: WinningConstraint.ParticipationPrizeGiven, nonWinningConstraint: NonWinningConstraint.GivenForFixedPrice, - fixedPrice: new BN(toLamports(attributes.priceFloor, mint) || 0), + fixedPrice: new BN( + toLamports(attributes.participationFixedPrice, mint) || 0, + ), }) : null, }); @@ -284,7 +296,9 @@ export const AuctionCreateView = () => { safetyDepositBoxIndex: tieredAttributes.items.length, winnerConstraint: WinningConstraint.ParticipationPrizeGiven, nonWinningConstraint: NonWinningConstraint.GivenForFixedPrice, - fixedPrice: new BN(toLamports(attributes.priceFloor, mint) || 0), + fixedPrice: new BN( + toLamports(attributes.participationFixedPrice, mint) || 0, + ), }) : null, }); @@ -309,6 +323,12 @@ export const AuctionCreateView = () => { ? attributes.items[0] : attributes.participationNFT, QUOTE_MINT, + new PriceFloor({ + type: attributes.priceFloor + ? PriceFloorType.Minimum + : PriceFloorType.None, + minPrice: new BN((attributes.priceFloor || 0) * LAMPORTS_PER_SOL), + }), ); setAuctionObj(_auctionObj); }; @@ -836,27 +856,55 @@ const PriceAuction = (props: { - + {props.attributes.category === AuctionCategory.Open && ( + + )} + {props.attributes.category != AuctionCategory.Open && ( + + )} diff --git a/rust/auction/program/src/processor/place_bid.rs b/rust/auction/program/src/processor/place_bid.rs index 236fd349dfc..cdff99fea61 100644 --- a/rust/auction/program/src/processor/place_bid.rs +++ b/rust/auction/program/src/processor/place_bid.rs @@ -235,7 +235,7 @@ pub fn place_bid<'r, 'b: 'r>( args.amount, min[0] ); - if args.amount <= min[0] { + if args.amount < min[0] { return Err(AuctionError::BidTooSmall.into()); } }