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>
This commit is contained in:
Jordan Prince 2021-06-15 11:31:51 -05:00 committed by GitHub
parent 7a7d631621
commit 377f6cd521
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 189 additions and 90 deletions

View File

@ -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<any, any>([
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,
}),
),
);

View File

@ -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 =

View File

@ -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,

View File

@ -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())
}

View File

@ -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,7 +104,8 @@ const Countdown = ({ state }: { state?: CountdownState }) => {
>
Time left
</div>
{state && (isEnded(state) ? (
{state &&
(isEnded(state) ? (
<Row style={{ width: '100%' }}>
<div className="cd-number">This auction has ended</div>
</Row>
@ -107,7 +114,9 @@ const Countdown = ({ state }: { state?: CountdownState }) => {
{state && state.days > 0 && (
<Col>
<div className="cd-number">
{state.days < 10 && <span style={{ opacity: 0.2 }}>0</span>}
{state.days < 10 && (
<span style={{ opacity: 0.2 }}>0</span>
)}
{state.days}
<span style={{ opacity: 0.2 }}>:</span>
</div>
@ -116,7 +125,9 @@ const Countdown = ({ state }: { state?: CountdownState }) => {
)}
<Col>
<div className="cd-number">
{state.hours < 10 && <span style={{ opacity: 0.2 }}>0</span>}
{state.hours < 10 && (
<span style={{ opacity: 0.2 }}>0</span>
)}
{state.hours}
<span style={{ opacity: 0.2 }}>:</span>
</div>
@ -128,7 +139,9 @@ const Countdown = ({ state }: { state?: CountdownState }) => {
<span style={{ opacity: 0.2 }}>0</span>
)}
{state.minutes}
{state.days === 0 && <span style={{ opacity: 0.2 }}>:</span>}
{state.days === 0 && (
<span style={{ opacity: 0.2 }}>:</span>
)}
</div>
<div className="cd-label">mins</div>
</Col>
@ -150,4 +163,3 @@ const Countdown = ({ state }: { state?: CountdownState }) => {
</>
);
};

View File

@ -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,6 +856,33 @@ const PriceAuction = (props: {
</Row>
<Row className="content-action">
<Col className="section" xl={24}>
{props.attributes.category === AuctionCategory.Open && (
<label className="action-field">
<span className="field-title">Price</span>
<span className="field-info">
This is an optional fixed price that non-winners will pay for
your Participation NFT.
</span>
<Input
type="number"
min={0}
autoFocus
className="input"
placeholder="Fixed Price"
prefix="◎"
suffix="SOL"
onChange={info =>
props.setAttributes({
...props.attributes,
// Do both, since we know this is the only item being sold.
participationFixedPrice: parseFloat(info.target.value),
priceFloor: parseFloat(info.target.value),
})
}
/>
</label>
)}
{props.attributes.category != AuctionCategory.Open && (
<label className="action-field">
<span className="field-title">Price Floor</span>
<span className="field-info">
@ -857,6 +904,7 @@ const PriceAuction = (props: {
}
/>
</label>
)}
<label className="action-field">
<span className="field-title">Tick Size</span>
<span className="field-info">
@ -1543,6 +1591,28 @@ const ParticipationStep = (props: {
>
Select Participation NFT
</ArtSelector>
<label className="action-field">
<span className="field-title">Price</span>
<span className="field-info">
This is an optional fixed price that non-winners will pay for your
Participation NFT.
</span>
<Input
type="number"
min={0}
autoFocus
className="input"
placeholder="Fixed Price"
prefix="◎"
suffix="SOL"
onChange={info =>
props.setAttributes({
...props.attributes,
participationFixedPrice: parseFloat(info.target.value),
})
}
/>
</label>
</Col>
</Row>
<Row>

View File

@ -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());
}
}