Instant Sale Create Flow

This commit is contained in:
adamjeffries 2021-08-25 15:43:05 -05:00
parent 0b8feaed9a
commit f684a0490c
No known key found for this signature in database
GPG Key ID: 1A61002100E0C66D
2 changed files with 243 additions and 333 deletions

View File

@ -2,11 +2,6 @@ import {
findProgramAddress,
getAuctionExtended,
programIds,
VAULT_PREFIX,
} from '@oyster/common';
import {
findProgramAddress,
programIds,
StringPublicKey,
toPublicKey,
VAULT_PREFIX,

View File

@ -36,7 +36,6 @@ import {
import { Connection, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { MintLayout } from '@solana/spl-token';
import { useHistory, useParams } from 'react-router-dom';
import { capitalize } from 'lodash';
import { WinningConfigType, AmountRange } from '../../models/metaplex';
import moment from 'moment';
import {
@ -57,6 +56,7 @@ const { Step } = Steps;
const { ZERO } = constants;
export enum AuctionCategory {
InstantSale,
Limited,
Single,
Open,
@ -98,7 +98,6 @@ export interface AuctionState {
//////////////////
category: AuctionCategory;
saleType?: 'auction' | 'sale';
price?: number;
priceFloor?: number;
@ -133,19 +132,19 @@ export const AuctionCreateView = () => {
const [step, setStep] = useState<number>(0);
const [stepsVisible, setStepsVisible] = useState<boolean>(true);
const [auctionObj, setAuctionObj] = useState<
| {
vault: StringPublicKey;
auction: StringPublicKey;
auctionManager: StringPublicKey;
}
| undefined
>(undefined);
const [auctionObj, setAuctionObj] =
useState<
| {
vault: StringPublicKey;
auction: StringPublicKey;
auctionManager: StringPublicKey;
}
| undefined
>(undefined);
const [attributes, setAttributes] = useState<AuctionState>({
reservationPrice: 0,
items: [],
category: AuctionCategory.Open,
saleType: 'auction',
auctionDurationType: 'minutes',
gapTimeType: 'minutes',
winnersCount: 1,
@ -445,6 +444,14 @@ export const AuctionCreateView = () => {
/>
);
const instantSaleStep = (
<InstantSaleStep
attributes={attributes}
setAttributes={setAttributes}
confirm={() => gotoNextStep()}
/>
);
const copiesStep = (
<CopiesStep
attributes={attributes}
@ -461,16 +468,8 @@ export const AuctionCreateView = () => {
/>
);
const typeStep = (
<SaleTypeStep
attributes={attributes}
setAttributes={setAttributes}
confirm={() => gotoNextStep()}
/>
);
const priceStep = (
<PriceStep
const priceAuction = (
<PriceAuction
attributes={attributes}
setAttributes={setAttributes}
confirm={() => gotoNextStep()}
@ -486,7 +485,7 @@ export const AuctionCreateView = () => {
);
const endingStep = (
<EndingPhaseStep
<EndingPhaseAuction
attributes={attributes}
setAttributes={setAttributes}
confirm={() => gotoNextStep()}
@ -529,11 +528,17 @@ export const AuctionCreateView = () => {
const congratsStep = <Congrats auction={auctionObj} />;
const stepsByCategory = {
[AuctionCategory.InstantSale]: [
['Category', categoryStep],
['Instant Sale', instantSaleStep],
['Review', reviewStep], // Update for clarity
['Publish', waitStep], // Just the loading screen
[undefined, congratsStep],
],
[AuctionCategory.Limited]: [
['Category', categoryStep],
['Copies', copiesStep],
['Sale Type', typeStep],
['Price', priceStep],
['Price', priceAuction],
['Initial Phase', initialStep],
['Ending Phase', endingStep],
['Participation NFT', participationStep],
@ -544,7 +549,7 @@ export const AuctionCreateView = () => {
[AuctionCategory.Single]: [
['Category', categoryStep],
['Copies', copiesStep],
['Price', priceStep],
['Price', priceAuction],
['Initial Phase', initialStep],
['Ending Phase', endingStep],
['Participation NFT', participationStep],
@ -555,7 +560,7 @@ export const AuctionCreateView = () => {
[AuctionCategory.Open]: [
['Category', categoryStep],
['Copies', copiesStep],
['Price', priceStep],
['Price', priceAuction],
['Initial Phase', initialStep],
['Ending Phase', endingStep],
['Review', reviewStep],
@ -566,7 +571,7 @@ export const AuctionCreateView = () => {
['Category', categoryStep],
['Winners', winnersStep],
['Tiers', tierTableStep],
['Price', priceStep],
['Price', priceAuction],
['Initial Phase', initialStep],
['Ending Phase', endingStep],
['Participation NFT', participationStep],
@ -627,6 +632,20 @@ const CategoryStep = (props: {
</Row>
<Row justify={width < 768 ? 'center' : 'start'}>
<Col>
<Row>
<Button
className="type-btn"
size="large"
onClick={() => props.confirm(AuctionCategory.InstantSale)}
>
<div>
<div>Instant Sale</div>
<div className="type-btn-description">
At a fixed price, sell a single Master NFT or copies of it
</div>
</div>
</Button>
</Row>
<Row>
<Button
className="type-btn"
@ -691,6 +710,145 @@ const CategoryStep = (props: {
);
};
const InstantSaleStep = (props: {
attributes: AuctionState;
setAttributes: (attr: AuctionState) => void;
confirm: () => void;
}) => {
const [copiesChecked, setCopiesChecked] = useState(false);
const copiesEnabled = React.useMemo(
() => !!props.attributes?.items?.[0]?.masterEdition?.info?.maxSupply,
[props.attributes?.items?.[0]],
);
let artistFilter = (i: SafetyDepositDraft) =>
!(i.metadata.info.data.creators || []).find((c: Creator) => !c.verified);
return (
<>
<Row className="call-to-action" style={{ marginBottom: 0 }}>
<h2>Select which item to sell:</h2>
</Row>
<Row className="content-action">
<Col xl={24}>
<ArtSelector
filter={artistFilter}
selected={props.attributes.items}
setSelected={items => {
props.setAttributes({ ...props.attributes, items });
}}
allowMultiple={false}
>
Select NFT
</ArtSelector>
<label className="action-field">
<Checkbox
defaultChecked={false}
checked={copiesChecked}
disabled={!copiesEnabled}
onChange={e => setCopiesChecked(e.target.checked)}
>
<span className="field-title">
Create copies of a Master Edition NFT?
</span>
</Checkbox>
{copiesChecked && copiesEnabled && (
<>
<span className="field-info">
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 =>
props.setAttributes({
...props.attributes,
editions: parseInt(info.target.value),
})
}
/>
</>
)}
</label>
<label className="action-field">
<span className="field-title">Price</span>
<span className="field-info">
This is the instant sale price for your item.
</span>
<Input
type="number"
min={0}
autoFocus
className="input"
placeholder="Price"
prefix="◎"
suffix="SOL"
onChange={info =>
props.setAttributes({
...props.attributes,
priceFloor: parseFloat(info.target.value),
instantSalePrice: parseFloat(info.target.value),
})
}
/>
</label>
<div className="action-field">
<span className="field-title">Sale Duration</span>
<span className="field-info">
This is how long the sale will last for.
</span>
<Input
addonAfter={
<Select
defaultValue={props.attributes.auctionDurationType}
onChange={value =>
props.setAttributes({
...props.attributes,
auctionDurationType: value,
})
}
>
<Option value="minutes">Minutes</Option>
<Option value="hours">Hours</Option>
<Option value="days">Days</Option>
</Select>
}
autoFocus
type="number"
className="input"
placeholder="Set the sale duration"
onChange={info =>
props.setAttributes({
...props.attributes,
auctionDuration: parseInt(info.target.value),
})
}
/>
</div>
</Col>
</Row>
<Row>
<Button
type="primary"
size="large"
onClick={() => {
props.confirm();
}}
className="action-btn"
>
Continue
</Button>
</Row>
</>
);
};
const CopiesStep = (props: {
attributes: AuctionState;
setAttributes: (attr: AuctionState) => void;
@ -823,137 +981,6 @@ const NumberOfWinnersStep = (props: {
);
};
const SaleTypeStep = (props: {
attributes: AuctionState;
setAttributes: (attr: AuctionState) => void;
confirm: () => void;
}) => {
return (
<>
<Row className="call-to-action">
<h2>Sale Type</h2>
<p>Sell a limited copy or copies of a single Master NFT.</p>
</Row>
<Row className="content-action">
<Col className="section" xl={24}>
<label className="action-field">
<span className="field-title">
How do you want to sell your NFT(s)?
</span>
<Radio.Group
defaultValue={props.attributes.saleType}
onChange={info =>
props.setAttributes({
...props.attributes,
saleType: info.target.value,
})
}
>
<Radio className="radio-field" value="auction">
Auction
</Radio>
<div className="radio-subtitle">
Allow bidding on your NFT(s).
</div>
</Radio.Group>
<Radio.Group
defaultValue={props.attributes.saleType}
onChange={info =>
props.setAttributes({
...props.attributes,
saleType: info.target.value,
})
}
>
<Radio className="radio-field" value="auction">
Instant Sale
</Radio>
<div className="radio-subtitle">
Instant purchase and redemption of your NFT.
</div>
</Radio.Group>
</label>
</Col>
</Row>
<Row>
<Button
type="primary"
size="large"
onClick={props.confirm}
className="action-btn"
>
Continue
</Button>
</Row>
</>
);
};
const PriceStep = (props: {
attributes: AuctionState;
setAttributes: (attr: AuctionState) => void;
confirm: () => void;
}) => {
return (
<>
{props.attributes.saleType === 'auction' ? (
<PriceAuction {...props} />
) : (
<PriceSale {...props} />
)}
</>
);
};
const PriceSale = (props: {
attributes: AuctionState;
setAttributes: (attr: AuctionState) => void;
confirm: () => void;
}) => {
return (
<>
<Row className="call-to-action">
<h2>Price</h2>
<p>Set the fixed price for your instant sale.</p>
</Row>
<Row className="content-action">
<label className="action-field">
<span className="field-title">Sale price</span>
<span className="field-info">
This is the price of purchasing the item(s).
</span>
<Input
type="number"
min={0}
autoFocus
className="input"
placeholder="Price"
prefix="◎"
suffix="SOL"
onChange={info =>
props.setAttributes({
...props.attributes,
priceFloor: parseFloat(info.target.value),
instantSalePrice: parseFloat(info.target.value),
})
}
/>
</label>
</Row>
<Row>
<Button
type="primary"
size="large"
onClick={props.confirm}
className="action-btn"
>
Continue
</Button>
</Row>
</>
);
};
const PriceAuction = (props: {
attributes: AuctionState;
setAttributes: (attr: AuctionState) => void;
@ -1103,13 +1130,13 @@ const InitialPhaseStep = (props: {
<>
<Row className="call-to-action">
<h2>Initial Phase</h2>
<p>Set the terms for your {props.attributes.saleType}.</p>
<p>Set the terms for your auction.</p>
</Row>
<Row className="content-action">
<Col className="section" xl={24}>
<label className="action-field">
<span className="field-title">
When do you want the {props.attributes.saleType} to begin?
When do you want the auction to begin?
</span>
<Radio.Group
defaultValue="now"
@ -1134,9 +1161,7 @@ const InitialPhaseStep = (props: {
{!startNow && (
<>
<label className="action-field">
<span className="field-title">
{capitalize(props.attributes.saleType)} Start Date
</span>
<span className="field-title">Auction Start Date</span>
{saleMoment && (
<DateTimePicker
momentObj={saleMoment}
@ -1215,22 +1240,6 @@ const InitialPhaseStep = (props: {
);
};
const EndingPhaseStep = (props: {
attributes: AuctionState;
setAttributes: (attr: AuctionState) => void;
confirm: () => void;
}) => {
return (
<>
{props.attributes.saleType === 'auction' ? (
<EndingPhaseAuction {...props} />
) : (
<EndingPhaseSale {...props} />
)}
</>
);
};
const EndingPhaseAuction = (props: {
attributes: AuctionState;
setAttributes: (attr: AuctionState) => void;
@ -1245,10 +1254,7 @@ const EndingPhaseAuction = (props: {
<Row className="content-action">
<Col className="section" xl={24}>
<div className="action-field">
<span className="field-title">
{props.attributes.saleType == 'auction' ? 'Auction' : 'Sale'}{' '}
Duration
</span>
<span className="field-title">Auction Duration</span>
<span className="field-info">
This is how long the auction will last for.
</span>
@ -1281,153 +1287,61 @@ const EndingPhaseAuction = (props: {
/>
</div>
{props.attributes.saleType == 'auction' && (
<>
<div className="action-field">
<span className="field-title">Gap Time</span>
<span className="field-info">
The final phase of the auction will begin when there is this
much time left on the countdown. Any bids placed during the
final phase will extend the end time by this same duration.
</span>
<Input
addonAfter={
<Select
defaultValue={props.attributes.gapTimeType}
onChange={value =>
props.setAttributes({
...props.attributes,
gapTimeType: value,
})
}
>
<Option value="minutes">Minutes</Option>
<Option value="hours">Hours</Option>
<Option value="days">Days</Option>
</Select>
}
type="number"
className="input"
placeholder="Set the gap time"
onChange={info =>
props.setAttributes({
...props.attributes,
gapTime: parseInt(info.target.value),
})
}
/>
</div>
<label className="action-field">
<span className="field-title">Tick Size for Ending Phase</span>
<span className="field-info">
In order for winners to move up in the auction, they must
place a bid thats at least this percentage higher than the
next highest bid.
</span>
<Input
type="number"
className="input"
placeholder="Percentage"
suffix="%"
onChange={info =>
props.setAttributes({
...props.attributes,
tickSizeEndingPhase: parseInt(info.target.value),
})
}
/>
</label>
</>
)}
</Col>
</Row>
<Row>
<Button
type="primary"
size="large"
onClick={props.confirm}
className="action-btn"
>
Continue
</Button>
</Row>
</>
);
};
const EndingPhaseSale = (props: {
attributes: AuctionState;
setAttributes: (attr: AuctionState) => void;
confirm: () => void;
}) => {
const startMoment = props.attributes.startSaleTS
? moment.unix(props.attributes.startSaleTS)
: moment();
const [untilSold, setUntilSold] = useState<boolean>(true);
const [endMoment, setEndMoment] = useState<moment.Moment | undefined>(
props.attributes.endTS ? moment.unix(props.attributes.endTS) : undefined,
);
useEffect(() => {
props.setAttributes({
...props.attributes,
endTS: endMoment && endMoment.unix(),
});
}, [endMoment]);
useEffect(() => {
if (untilSold) setEndMoment(undefined);
else setEndMoment(startMoment);
}, [untilSold]);
return (
<>
<Row className="call-to-action">
<h2>Ending Phase</h2>
<p>Set the terms for your sale.</p>
</Row>
<Row className="content-action">
<Col className="section" xl={24}>
<label className="action-field">
<span className="field-title">
When do you want the sale to end?
<div className="action-field">
<span className="field-title">Gap Time</span>
<span className="field-info">
The final phase of the auction will begin when there is this much
time left on the countdown. Any bids placed during the final phase
will extend the end time by this same duration.
</span>
<Radio.Group
defaultValue="now"
onChange={info => setUntilSold(info.target.value === 'now')}
>
<Radio className="radio-field" value="now">
Until sold
</Radio>
<div className="radio-subtitle">
The sale will end once the supply goes to zero.
</div>
<Radio className="radio-field" value="later">
At a specified date
</Radio>
<div className="radio-subtitle">
The sale will end at this date, regardless if there is remaining
supply.
</div>
</Radio.Group>
</label>
<Input
addonAfter={
<Select
defaultValue={props.attributes.gapTimeType}
onChange={value =>
props.setAttributes({
...props.attributes,
gapTimeType: value,
})
}
>
<Option value="minutes">Minutes</Option>
<Option value="hours">Hours</Option>
<Option value="days">Days</Option>
</Select>
}
type="number"
className="input"
placeholder="Set the gap time"
onChange={info =>
props.setAttributes({
...props.attributes,
gapTime: parseInt(info.target.value),
})
}
/>
</div>
{!untilSold && (
<label className="action-field">
<span className="field-title">End Date</span>
{endMoment && (
<DateTimePicker
momentObj={endMoment}
setMomentObj={setEndMoment}
datePickerProps={{
disabledDate: (current: moment.Moment) =>
current && current < startMoment,
}}
/>
)}
</label>
)}
<label className="action-field">
<span className="field-title">Tick Size for Ending Phase</span>
<span className="field-info">
In order for winners to move up in the auction, they must place a
bid thats at least this percentage higher than the next highest
bid.
</span>
<Input
type="number"
className="input"
placeholder="Percentage"
suffix="%"
onChange={info =>
props.setAttributes({
...props.attributes,
tickSizeEndingPhase: parseInt(info.target.value),
})
}
/>
</label>
</Col>
</Row>
<Row>
@ -1892,6 +1806,7 @@ const ReviewStep = (props: {
startListTS: props.attributes.startListTS || moment().unix(),
startSaleTS: props.attributes.startSaleTS || moment().unix(),
});
console.log('attributes', props.attributes);
props.confirm();
}}
className="action-btn"