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, findProgramAddress,
getAuctionExtended, getAuctionExtended,
programIds, programIds,
VAULT_PREFIX,
} from '@oyster/common';
import {
findProgramAddress,
programIds,
StringPublicKey, StringPublicKey,
toPublicKey, toPublicKey,
VAULT_PREFIX, VAULT_PREFIX,

View File

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