Merge pull request #644 from m-sebastiyan/feature/open-edition-instant-sale

feat: implement open edition instant sale
This commit is contained in:
Adam Jeffries 2021-10-07 09:36:56 -05:00 committed by GitHub
commit f4fbe26bd0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 147 additions and 71 deletions

View File

@ -21,6 +21,7 @@ import {
MAX_EDITION_LEN, MAX_EDITION_LEN,
useWalletModal, useWalletModal,
VaultState, VaultState,
BidStateType,
} from '@oyster/common'; } from '@oyster/common';
import { useWallet } from '@solana/wallet-adapter-react'; import { useWallet } from '@solana/wallet-adapter-react';
import { AuctionView, useBidsForAuction, useUserBalance } from '../../hooks'; import { AuctionView, useBidsForAuction, useUserBalance } from '../../hooks';
@ -254,14 +255,23 @@ export const AuctionCard = ({
const isAuctionNotStarted = const isAuctionNotStarted =
auctionView.auction.info.state === AuctionState.Created; auctionView.auction.info.state === AuctionState.Created;
//if instant sale auction bid and claimed hide buttons const isOpenEditionSale =
if ( auctionView.auction.info.bidState.type === BidStateType.OpenEdition;
(auctionView.isInstantSale && const doesInstantSaleHasNoItems =
Number(auctionView.myBidderPot?.info.emptied) !== 0 && Number(auctionView.myBidderPot?.info.emptied) !== 0 &&
isAuctionManagerAuthorityNotWalletOwner && auctionView.auction.info.bidState.max.toNumber() === bids.length;
auctionView.auction.info.bidState.max.toNumber() === bids.length) ||
auctionView.vault.info.state === VaultState.Deactivated const shouldHideInstantSale =
) { !isOpenEditionSale &&
auctionView.isInstantSale &&
isAuctionManagerAuthorityNotWalletOwner &&
doesInstantSaleHasNoItems;
const shouldHide =
shouldHideInstantSale ||
auctionView.vault.info.state === VaultState.Deactivated;
if (shouldHide) {
return <></>; return <></>;
} }
@ -515,13 +525,16 @@ export const AuctionCard = ({
const instantSale = async () => { const instantSale = async () => {
setLoading(true); setLoading(true);
const instantSalePrice = const instantSalePrice =
auctionView.auctionDataExtended?.info.instantSalePrice; auctionView.auctionDataExtended?.info.instantSalePrice;
const winningConfigType = const winningConfigType =
auctionView.participationItem?.winningConfigType ||
auctionView.items[0][0].winningConfigType; auctionView.items[0][0].winningConfigType;
const isAuctionItemMaster = const isAuctionItemMaster = [
winningConfigType === WinningConfigType.FullRightsTransfer || WinningConfigType.FullRightsTransfer,
winningConfigType === WinningConfigType.TokenOnlyTransfer; WinningConfigType.TokenOnlyTransfer,
].includes(winningConfigType);
const allowBidToPublic = const allowBidToPublic =
myPayingAccount && myPayingAccount &&
!auctionView.myBidderPot && !auctionView.myBidderPot &&
@ -532,7 +545,10 @@ export const AuctionCard = ({
isAuctionItemMaster; isAuctionItemMaster;
// Placing a "bid" of the full amount results in a purchase to redeem. // Placing a "bid" of the full amount results in a purchase to redeem.
if (instantSalePrice && (allowBidToPublic || allowBidToAuctionOwner)) { if (
instantSalePrice &&
(allowBidToPublic || allowBidToAuctionOwner)
) {
try { try {
const bid = await sendPlaceBid( const bid = await sendPlaceBid(
connection, connection,

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState, useMemo, useCallback } from 'react';
import { import {
Divider, Divider,
Steps, Steps,
@ -67,6 +67,12 @@ export enum AuctionCategory {
Tiered, Tiered,
} }
enum InstantSaleType {
Limited,
Single,
Open,
}
interface TierDummyEntry { interface TierDummyEntry {
safetyDepositBoxIndex: number; safetyDepositBoxIndex: number;
amount: number; amount: number;
@ -123,6 +129,7 @@ export interface AuctionState {
winnersCount: number; winnersCount: number;
instantSalePrice?: number; instantSalePrice?: number;
instantSaleType?: InstantSaleType;
} }
export const AuctionCreateView = () => { export const AuctionCreateView = () => {
@ -173,26 +180,47 @@ export const AuctionCreateView = () => {
const createAuction = async () => { const createAuction = async () => {
let winnerLimit: WinnerLimit; let winnerLimit: WinnerLimit;
if (attributes.category === AuctionCategory.InstantSale) { if (
if (attributes.items.length > 0) { attributes.category === AuctionCategory.InstantSale &&
const item = attributes.items[0]; attributes.instantSaleType === InstantSaleType.Open
if (!attributes.editions) { ) {
const { items, instantSalePrice } = attributes;
if (items.length > 0 && items[0].participationConfig) {
items[0].participationConfig.fixedPrice = new BN(
toLamports(instantSalePrice, mint) || 0,
);
}
winnerLimit = new WinnerLimit({
type: WinnerLimitType.Unlimited,
usize: ZERO,
});
} else if (attributes.category === AuctionCategory.InstantSale) {
const { items, editions } = attributes;
if (items.length > 0) {
const item = items[0];
if (!editions) {
item.winningConfigType = item.winningConfigType =
item.metadata.info.updateAuthority === item.metadata.info.updateAuthority ===
(wallet?.publicKey || SystemProgram.programId).toBase58() (wallet?.publicKey || SystemProgram.programId).toBase58()
? WinningConfigType.FullRightsTransfer ? WinningConfigType.FullRightsTransfer
: WinningConfigType.TokenOnlyTransfer; : WinningConfigType.TokenOnlyTransfer;
} }
item.amountRanges = [ item.amountRanges = [
new AmountRange({ new AmountRange({
amount: new BN(1), amount: new BN(1),
length: new BN(attributes.editions || 1), length: new BN(editions || 1),
}), }),
]; ];
} }
winnerLimit = new WinnerLimit({ winnerLimit = new WinnerLimit({
type: WinnerLimitType.Capped, type: WinnerLimitType.Capped,
usize: new BN(attributes.editions || 1), usize: new BN(editions || 1),
}); });
} else if (attributes.category === AuctionCategory.Open) { } else if (attributes.category === AuctionCategory.Open) {
if ( if (
@ -447,19 +475,25 @@ export const AuctionCreateView = () => {
name: null, name: null,
}; };
const isOpenEdition =
attributes.category === AuctionCategory.Open ||
attributes.instantSaleType === InstantSaleType.Open;
const safetyDepositDrafts = isOpenEdition
? []
: attributes.category !== AuctionCategory.Tiered
? attributes.items
: tieredAttributes.items;
const participationSafetyDepositDraft = isOpenEdition
? attributes.items[0]
: attributes.participationNFT;
const _auctionObj = await createAuctionManager( const _auctionObj = await createAuctionManager(
connection, connection,
wallet, wallet,
whitelistedCreatorsByCreator, whitelistedCreatorsByCreator,
auctionSettings, auctionSettings,
attributes.category === AuctionCategory.Open safetyDepositDrafts,
? [] participationSafetyDepositDraft,
: attributes.category !== AuctionCategory.Tiered
? attributes.items
: tieredAttributes.items,
attributes.category === AuctionCategory.Open
? attributes.items[0]
: attributes.participationNFT,
QUOTE_MINT.toBase58(), QUOTE_MINT.toBase58(),
); );
setAuctionObj(_auctionObj); setAuctionObj(_auctionObj);
@ -743,19 +777,28 @@ const CategoryStep = (props: {
); );
}; };
const InstantSaleStep = (props: { const InstantSaleStep = ({
attributes,
setAttributes,
confirm,
}: {
attributes: AuctionState; attributes: AuctionState;
setAttributes: (attr: AuctionState) => void; setAttributes: (attr: AuctionState) => void;
confirm: () => void; confirm: () => void;
}) => { }) => {
const [copiesChecked, setCopiesChecked] = useState(false); const copiesEnabled = useMemo(
const copiesEnabled = React.useMemo( () => !!attributes?.items?.[0]?.masterEdition?.info?.maxSupply,
() => !!props.attributes?.items?.[0]?.masterEdition?.info?.maxSupply, [attributes?.items?.[0]],
[props.attributes?.items?.[0]], );
const artistFilter = useCallback(
(i: SafetyDepositDraft) =>
!(i.metadata.info.data.creators || []).some((c: Creator) => !c.verified),
[],
); );
let artistFilter = (i: SafetyDepositDraft) => const isLimitedEdition =
!(i.metadata.info.data.creators || []).find((c: Creator) => !c.verified); attributes.instantSaleType === InstantSaleType.Limited;
const shouldRenderSelect = attributes.items.length > 0;
return ( return (
<> <>
@ -767,46 +810,63 @@ const InstantSaleStep = (props: {
<Col xl={24}> <Col xl={24}>
<ArtSelector <ArtSelector
filter={artistFilter} filter={artistFilter}
selected={props.attributes.items} selected={attributes.items}
setSelected={items => { setSelected={items => {
props.setAttributes({ ...props.attributes, items }); setAttributes({ ...attributes, items });
}} }}
allowMultiple={false} allowMultiple={false}
> >
Select NFT Select NFT
</ArtSelector> </ArtSelector>
<label className="action-field"> {shouldRenderSelect && (
<Checkbox <label className="action-field">
defaultChecked={false} <Select
checked={copiesChecked} defaultValue={
disabled={!copiesEnabled} attributes.instantSaleType || InstantSaleType.Single
onChange={e => setCopiesChecked(e.target.checked)} }
> onChange={value =>
<span className="field-title"> setAttributes({
Create copies of a Master Edition NFT? ...attributes,
</span> instantSaleType: value,
</Checkbox> })
{copiesChecked && copiesEnabled && ( }
<> >
<span className="field-info"> <Option value={InstantSaleType.Single}>
Each copy will be given unique edition number e.g. 1 of 30 Sell unique token
</span> </Option>
<Input {copiesEnabled && (
autoFocus <Option value={InstantSaleType.Limited}>
className="input" Sell limited number of copies
placeholder="Enter number of copies sold" </Option>
allowClear )}
onChange={info => {!copiesEnabled && (
props.setAttributes({ <Option value={InstantSaleType.Open}>
...props.attributes, Sell unlimited number of copies
editions: parseInt(info.target.value), </Option>
}) )}
} </Select>
/> {isLimitedEdition && (
</> <>
)} <span className="field-info">
</label> Each copy will be given unique edition number e.g. 1 of 30
</span>
<Input
autoFocus
className="input"
placeholder="Enter number of copies sold"
allowClear
onChange={info =>
setAttributes({
...attributes,
editions: parseInt(info.target.value),
})
}
/>
</>
)}
</label>
)}
<label className="action-field"> <label className="action-field">
<span className="field-title">Price</span> <span className="field-title">Price</span>
@ -822,8 +882,8 @@ const InstantSaleStep = (props: {
prefix="◎" prefix="◎"
suffix="SOL" suffix="SOL"
onChange={info => onChange={info =>
props.setAttributes({ setAttributes({
...props.attributes, ...attributes,
priceFloor: parseFloat(info.target.value), priceFloor: parseFloat(info.target.value),
instantSalePrice: parseFloat(info.target.value), instantSalePrice: parseFloat(info.target.value),
}) })
@ -837,7 +897,7 @@ const InstantSaleStep = (props: {
type="primary" type="primary"
size="large" size="large"
onClick={() => { onClick={() => {
props.confirm(); confirm();
}} }}
className="action-btn" className="action-btn"
> >

View File

@ -25,7 +25,7 @@ export const AuctionListView = () => {
const auctions = useAuctions(AuctionViewState.Live); const auctions = useAuctions(AuctionViewState.Live);
const auctionsEnded = [ const auctionsEnded = [
...useAuctions(AuctionViewState.Ended), ...useAuctions(AuctionViewState.Ended),
...useAuctions(AuctionViewState.BuyNow) ...useAuctions(AuctionViewState.BuyNow),
]; ];
const [activeKey, setActiveKey] = useState(LiveAuctionViewState.All); const [activeKey, setActiveKey] = useState(LiveAuctionViewState.All);
const { isLoading } = useMeta(); const { isLoading } = useMeta();