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:
parent
7a7d631621
commit
377f6cd521
|
@ -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,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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 }) => {
|
|||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue