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 &&
auctionView.auction.info.bidState.max.toNumber() === bids.length;
const shouldHideInstantSale =
!isOpenEditionSale &&
auctionView.isInstantSale &&
isAuctionManagerAuthorityNotWalletOwner && isAuctionManagerAuthorityNotWalletOwner &&
auctionView.auction.info.bidState.max.toNumber() === bids.length) || doesInstantSaleHasNoItems;
auctionView.vault.info.state === VaultState.Deactivated
) { 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,27 +810,43 @@ 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>
{shouldRenderSelect && (
<label className="action-field"> <label className="action-field">
<Checkbox <Select
defaultChecked={false} defaultValue={
checked={copiesChecked} attributes.instantSaleType || InstantSaleType.Single
disabled={!copiesEnabled} }
onChange={e => setCopiesChecked(e.target.checked)} onChange={value =>
setAttributes({
...attributes,
instantSaleType: value,
})
}
> >
<span className="field-title"> <Option value={InstantSaleType.Single}>
Create copies of a Master Edition NFT? Sell unique token
</span> </Option>
</Checkbox> {copiesEnabled && (
{copiesChecked && copiesEnabled && ( <Option value={InstantSaleType.Limited}>
Sell limited number of copies
</Option>
)}
{!copiesEnabled && (
<Option value={InstantSaleType.Open}>
Sell unlimited number of copies
</Option>
)}
</Select>
{isLimitedEdition && (
<> <>
<span className="field-info"> <span className="field-info">
Each copy will be given unique edition number e.g. 1 of 30 Each copy will be given unique edition number e.g. 1 of 30
@ -798,8 +857,8 @@ const InstantSaleStep = (props: {
placeholder="Enter number of copies sold" placeholder="Enter number of copies sold"
allowClear allowClear
onChange={info => onChange={info =>
props.setAttributes({ setAttributes({
...props.attributes, ...attributes,
editions: parseInt(info.target.value), editions: parseInt(info.target.value),
}) })
} }
@ -807,6 +866,7 @@ const InstantSaleStep = (props: {
</> </>
)} )}
</label> </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();