Index and show pages for auctions

This commit is contained in:
Jordan Prince 2021-04-26 16:59:16 -05:00
parent 2f036f7009
commit a46537221a
14 changed files with 583 additions and 136 deletions

View File

@ -45,6 +45,10 @@ export class BidState {
} }
} }
export const decodeAuction = (buffer: Buffer) => {
return deserializeBorsh(AUCTION_SCHEMA, AuctionData, buffer) as AuctionData;
};
export const BASE_AUCTION_DATA_SIZE = 32 + 32 + 32 + 8 + 8 + 1 + 9 + 9 + 9 + 9; export const BASE_AUCTION_DATA_SIZE = 32 + 32 + 32 + 8 + 8 + 1 + 9 + 9 + 9 + 9;
export class AuctionData { export class AuctionData {
@ -67,6 +71,9 @@ export class AuctionData {
/// Gap time is the amount of time in slots after the previous bid at which the auction ends. /// Gap time is the amount of time in slots after the previous bid at which the auction ends.
endAuctionGap?: BN; endAuctionGap?: BN;
/// Used for precalculation on the front end, not a backend key
auctionManagerKey?: PublicKey;
constructor(args: { constructor(args: {
authority: PublicKey; authority: PublicKey;
resource: PublicKey; resource: PublicKey;
@ -247,7 +254,7 @@ export const AUCTION_SCHEMA = new Map<any, any>([
['resource', 'pubkey'], ['resource', 'pubkey'],
['tokenMint', 'pubkey'], ['tokenMint', 'pubkey'],
['state', 'u8'], ['state', 'u8'],
['bidState', 'BidState'], ['bidState', BidState],
['lastBid', { kind: 'option', type: 'u64' }], ['lastBid', { kind: 'option', type: 'u64' }],
['endedAt', { kind: 'option', type: 'u64' }], ['endedAt', { kind: 'option', type: 'u64' }],
['endAuctionAt', { kind: 'option', type: 'u64' }], ['endAuctionAt', { kind: 'option', type: 'u64' }],

View File

@ -258,6 +258,14 @@ export const decodeVault = (buffer: Buffer) => {
return deserializeBorsh(VAULT_SCHEMA, Vault, buffer) as Vault; return deserializeBorsh(VAULT_SCHEMA, Vault, buffer) as Vault;
}; };
export const decodeSafetyDeposit = (buffer: Buffer) => {
return deserializeBorsh(
VAULT_SCHEMA,
SafetyDepositBox,
buffer,
) as SafetyDepositBox;
};
export async function initVault( export async function initVault(
allowFurtherShareCreation: boolean, allowFurtherShareCreation: boolean,
fractionalMint: PublicKey, fractionalMint: PublicKey,

View File

@ -1,33 +1,40 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { import { Row, Col, Divider, Button, InputNumber } from 'antd';
Row,
Col,
Divider,
Button,
InputNumber
} from 'antd'
import { Auction, Presale } from '../../types' import { Auction, Presale } from '../../types';
import './index.less' import './index.less';
import { getCountdown } from '../../utils/utils' import { getCountdown } from '../../utils/utils';
import { shortenAddress } from '@oyster/common'; import { shortenAddress, useConnection } from '@oyster/common';
import { AuctionView } from '../../hooks';
export const AuctionCard = ({ auction }: { auction: Auction }) => { export const AuctionCard = ({ auctionView }: { auctionView: AuctionView }) => {
const [hours, setHours] = useState<number>(23) const [hours, setHours] = useState<number>(23);
const [minutes, setMinutes] = useState<number>(59) const [minutes, setMinutes] = useState<number>(59);
const [seconds, setSeconds] = useState<number>(59) const [seconds, setSeconds] = useState<number>(59);
const [clock, setClock] = useState<number>(0);
const connection = useConnection();
useEffect(() => {
connection.getSlot().then(setClock);
}, [connection]);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
const { hours, minutes, seconds } = getCountdown(auction.endingTS) const slotDiff =
(auctionView.auction.info.endedAt?.toNumber() || 0) - clock;
setHours(hours) /* const { hours, minutes, seconds } = getCountdown(
setMinutes(minutes) auctionView.auction.info.endedAt?.toNumber(),
setSeconds(seconds) );
}, 1000)
return () => clearInterval(interval) setHours(hours);
}, []) setMinutes(minutes);
setSeconds(seconds);*/
setHours(1);
}, 1000);
return () => clearInterval(interval);
}, [clock]);
return ( return (
<div className="presale-card-container"> <div className="presale-card-container">
@ -50,8 +57,12 @@ export const AuctionCard = ({ auction }: { auction: Auction }) => {
</Col> </Col>
</Row> </Row>
<br /> <br />
<div className="info-content" style={{ color: 'rgba(255, 255, 255, 0.7)', fontSize: '0.9rem' }}> <div
Any bids placed in the last 15 minutes will extend the auction for another 15 minutes. className="info-content"
style={{ color: 'rgba(255, 255, 255, 0.7)', fontSize: '0.9rem' }}
>
Any bids placed in the last 15 minutes will extend the auction for
another 15 minutes.
</div> </div>
<br /> <br />
@ -70,7 +81,10 @@ export const AuctionCard = ({ auction }: { auction: Auction }) => {
// } // }
/> />
<div className="info-content" style={{ color: 'rgba(255, 255, 255, 0.7)', fontSize: '0.9rem' }}> <div
className="info-content"
style={{ color: 'rgba(255, 255, 255, 0.7)', fontSize: '0.9rem' }}
>
Your Balance: {0.0} (${0.0}) Your Balance: {0.0} (${0.0})
</div> </div>
@ -82,17 +96,21 @@ export const AuctionCard = ({ auction }: { auction: Auction }) => {
> >
PLACE BID PLACE BID
</Button> </Button>
</div> </div>
) );
} };
export const AuctionBidders = (auctionID: string) => { export const AuctionBidders = (auctionID: string) => {
const bids: any[] = []; const bids: any[] = [];
return <Col> return (
<Col>
{bids.map((bid, index) => { {bids.map((bid, index) => {
return <Row>{index+1}. {shortenAddress(bid.address)} {bid.amount}</Row> return (
<Row>
{index + 1}. {shortenAddress(bid.address)} {bid.amount}
</Row>
);
})} })}
</Col> </Col>
);
}; };

View File

@ -4,6 +4,7 @@ import {
useConnection, useConnection,
decodeMetadata, decodeMetadata,
decodeNameSymbolTuple, decodeNameSymbolTuple,
decodeAuction,
decodeEdition, decodeEdition,
decodeMasterEdition, decodeMasterEdition,
Metadata, Metadata,
@ -15,30 +16,55 @@ import {
Edition, Edition,
MasterEdition, MasterEdition,
NameSymbolTuple, NameSymbolTuple,
AuctionData,
SafetyDepositBox,
VaultKey,
decodeSafetyDeposit,
} from '@oyster/common'; } from '@oyster/common';
import { MintInfo } from '@solana/spl-token'; import { MintInfo } from '@solana/spl-token';
import { Connection, PublicKey, PublicKeyAndAccount } from '@solana/web3.js'; import { Connection, PublicKey, PublicKeyAndAccount } from '@solana/web3.js';
import BN from 'bn.js'; import BN from 'bn.js';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import {
AuctionManager,
AuctionManagerStatus,
decodeAuctionManager,
getAuctionManagerKey,
MetaplexKey,
} from '../models/metaplex';
const { MetadataKey } = actions; const { MetadataKey } = actions;
export interface MetaContextState { export interface MetaContextState {
metadata: ParsedAccount<Metadata>[]; metadata: ParsedAccount<Metadata>[];
metadataByMint: Record<string, ParsedAccount<Metadata>>;
nameSymbolTuples: Record<string, ParsedAccount<NameSymbolTuple>>; nameSymbolTuples: Record<string, ParsedAccount<NameSymbolTuple>>;
editions: Record<string, ParsedAccount<Edition>>; editions: Record<string, ParsedAccount<Edition>>;
masterEditions: Record<string, ParsedAccount<MasterEdition>>; masterEditions: Record<string, ParsedAccount<MasterEdition>>;
auctionManagers: Record<string, ParsedAccount<AuctionManager>>;
auctions: Record<string, ParsedAccount<AuctionData>>;
safetyDepositBoxesByVaultAndIndex: Record<
string,
ParsedAccount<SafetyDepositBox>
>;
} }
const MetaContext = React.createContext<MetaContextState>({ const MetaContext = React.createContext<MetaContextState>({
metadata: [], metadata: [],
metadataByMint: {},
nameSymbolTuples: {}, nameSymbolTuples: {},
masterEditions: {}, masterEditions: {},
editions: {}, editions: {},
auctionManagers: {},
auctions: {},
safetyDepositBoxesByVaultAndIndex: {},
}); });
export function MetaProvider({ children = null as any }) { export function MetaProvider({ children = null as any }) {
const connection = useConnection(); const connection = useConnection();
const [metadata, setMetadata] = useState<ParsedAccount<Metadata>[]>([]); const [metadata, setMetadata] = useState<ParsedAccount<Metadata>[]>([]);
const [metadataByMint, setMetadataByMint] = useState<
Record<string, ParsedAccount<Metadata>>
>({});
const [nameSymbolTuples, setNameSymbolTuples] = useState< const [nameSymbolTuples, setNameSymbolTuples] = useState<
Record<string, ParsedAccount<NameSymbolTuple>> Record<string, ParsedAccount<NameSymbolTuple>>
>({}); >({});
@ -48,12 +74,181 @@ export function MetaProvider({ children = null as any }) {
const [editions, setEditions] = useState< const [editions, setEditions] = useState<
Record<string, ParsedAccount<Edition>> Record<string, ParsedAccount<Edition>>
>({}); >({});
const [auctionManagers, setAuctionManagers] = useState<
Record<string, ParsedAccount<AuctionManager>>
>({});
const [auctions, setAuctions] = useState<
Record<string, ParsedAccount<AuctionData>>
>({});
const [
safetyDepositBoxesByVaultAndIndex,
setSafetyDepositBoxesByVaultAndIndex,
] = useState<Record<string, ParsedAccount<SafetyDepositBox>>>({});
useEffect(() => { useEffect(() => {
let dispose = () => {}; let dispose = () => {};
(async () => { (async () => {
const mintToMetadata = new Map<string, ParsedAccount<Metadata>>(); const processAuctions = async (a: PublicKeyAndAccount<Buffer>) => {
try {
const auction = await decodeAuction(a.account.data);
auction.auctionManagerKey = await getAuctionManagerKey(
auction.resource,
a.pubkey,
);
const account: ParsedAccount<AuctionData> = {
pubkey: a.pubkey,
account: a.account,
info: auction,
};
setAuctions(e => ({
...e,
[a.pubkey.toBase58()]: account,
}));
} catch {
// ignore errors
// add type as first byte for easier deserialization
}
};
const accounts = await connection.getProgramAccounts(
programIds().auction,
);
for (let i = 0; i < accounts.length; i++) {
await processAuctions(accounts[i]);
}
let subId = connection.onProgramAccountChange(
programIds().auction,
async info => {
const pubkey =
typeof info.accountId === 'string'
? new PublicKey((info.accountId as unknown) as string)
: info.accountId;
await processAuctions({
pubkey,
account: info.accountInfo,
});
},
);
dispose = () => {
connection.removeProgramAccountChangeListener(subId);
};
})();
return () => {
dispose();
};
}, [connection, setAuctions]);
useEffect(() => {
let dispose = () => {};
(async () => {
const processSafetyDeposits = async (a: PublicKeyAndAccount<Buffer>) => {
try {
if (a.account.data[0] == VaultKey.SafetyDepositBoxV1) {
const safetyDeposit = await decodeSafetyDeposit(a.account.data);
const account: ParsedAccount<SafetyDepositBox> = {
pubkey: a.pubkey,
account: a.account,
info: safetyDeposit,
};
setSafetyDepositBoxesByVaultAndIndex(e => ({
...e,
[safetyDeposit.vault.toBase58() +
'-' +
safetyDeposit.order]: account,
}));
}
} catch {
// ignore errors
// add type as first byte for easier deserialization
}
};
const accounts = await connection.getProgramAccounts(programIds().vault);
for (let i = 0; i < accounts.length; i++) {
await processSafetyDeposits(accounts[i]);
}
let subId = connection.onProgramAccountChange(
programIds().vault,
async info => {
const pubkey =
typeof info.accountId === 'string'
? new PublicKey((info.accountId as unknown) as string)
: info.accountId;
await processSafetyDeposits({
pubkey,
account: info.accountInfo,
});
},
);
dispose = () => {
connection.removeProgramAccountChangeListener(subId);
};
})();
return () => {
dispose();
};
}, [connection, setSafetyDepositBoxesByVaultAndIndex]);
useEffect(() => {
let dispose = () => {};
(async () => {
const processAuctionManagers = async (a: PublicKeyAndAccount<Buffer>) => {
try {
if (a.account.data[0] == MetaplexKey.AuctionManagerV1) {
const auctionManager = await decodeAuctionManager(a.account.data);
const account: ParsedAccount<AuctionManager> = {
pubkey: a.pubkey,
account: a.account,
info: auctionManager,
};
setAuctionManagers(e => ({
...e,
[a.pubkey.toBase58()]: account,
}));
}
} catch {
// ignore errors
// add type as first byte for easier deserialization
}
};
const accounts = await connection.getProgramAccounts(
programIds().metaplex,
);
for (let i = 0; i < accounts.length; i++) {
await processAuctionManagers(accounts[i]);
}
let subId = connection.onProgramAccountChange(
programIds().metaplex,
async info => {
const pubkey =
typeof info.accountId === 'string'
? new PublicKey((info.accountId as unknown) as string)
: info.accountId;
await processAuctionManagers({
pubkey,
account: info.accountInfo,
});
},
);
dispose = () => {
connection.removeProgramAccountChangeListener(subId);
};
})();
return () => {
dispose();
};
}, [connection, setAuctionManagers]);
useEffect(() => {
let dispose = () => {};
(async () => {
const processMetaData = async (meta: PublicKeyAndAccount<Buffer>) => { const processMetaData = async (meta: PublicKeyAndAccount<Buffer>) => {
try { try {
if (meta.account.data[0] == MetadataKey.MetadataV1) { if (meta.account.data[0] == MetadataKey.MetadataV1) {
@ -67,7 +262,10 @@ export function MetaProvider({ children = null as any }) {
account: meta.account, account: meta.account,
info: metadata, info: metadata,
}; };
mintToMetadata.set(metadata.mint.toBase58(), account); setMetadataByMint(e => ({
...e,
[metadata.mint.toBase58()]: account,
}));
} }
} else if (meta.account.data[0] == MetadataKey.EditionV1) { } else if (meta.account.data[0] == MetadataKey.EditionV1) {
const edition = decodeEdition(meta.account.data); const edition = decodeEdition(meta.account.data);
@ -113,20 +311,21 @@ export function MetaProvider({ children = null as any }) {
await processMetaData(accounts[i]); await processMetaData(accounts[i]);
} }
await queryExtendedMetadata(connection, setMetadata, mintToMetadata); await queryExtendedMetadata(connection, setMetadata, metadataByMint);
let subId = connection.onProgramAccountChange( let subId = connection.onProgramAccountChange(
programIds().metadata, programIds().metadata,
async info => { async info => {
const pubkey = typeof info.accountId === 'string' ? const pubkey =
new PublicKey((info.accountId as unknown) as string) : typeof info.accountId === 'string'
info.accountId; ? new PublicKey((info.accountId as unknown) as string)
: info.accountId;
await processMetaData({ await processMetaData({
pubkey, pubkey,
account: info.accountInfo, account: info.accountInfo,
}); });
queryExtendedMetadata(connection, setMetadata, mintToMetadata); queryExtendedMetadata(connection, setMetadata, metadataByMint);
}, },
); );
dispose = () => { dispose = () => {
@ -147,7 +346,16 @@ export function MetaProvider({ children = null as any }) {
return ( return (
<MetaContext.Provider <MetaContext.Provider
value={{ metadata, editions, masterEditions, nameSymbolTuples }} value={{
metadata,
editions,
masterEditions,
nameSymbolTuples,
auctionManagers,
auctions,
metadataByMint,
safetyDepositBoxesByVaultAndIndex,
}}
> >
{children} {children}
</MetaContext.Provider> </MetaContext.Provider>
@ -157,14 +365,14 @@ export function MetaProvider({ children = null as any }) {
const queryExtendedMetadata = async ( const queryExtendedMetadata = async (
connection: Connection, connection: Connection,
setMetadata: (metadata: ParsedAccount<Metadata>[]) => void, setMetadata: (metadata: ParsedAccount<Metadata>[]) => void,
mintToMeta: Map<string, ParsedAccount<Metadata>>, mintToMeta: Record<string, ParsedAccount<Metadata>>,
) => { ) => {
const mintToMetadata = new Map<string, ParsedAccount<Metadata>>(mintToMeta); const mintToMetadata = { ...mintToMeta };
const extendedMetadataFetch = new Map<string, Promise<any>>(); const extendedMetadataFetch = new Map<string, Promise<any>>();
const mints = await getMultipleAccounts( const mints = await getMultipleAccounts(
connection, connection,
[...mintToMetadata.keys()].filter(k => !cache.get(k)), [...Object.keys(mintToMetadata)].filter(k => !cache.get(k)),
'single', 'single',
); );
mints.keys.forEach((key, index) => { mints.keys.forEach((key, index) => {
@ -176,9 +384,9 @@ const queryExtendedMetadata = async (
) as ParsedAccount<MintInfo>; ) as ParsedAccount<MintInfo>;
if (mint.info.supply.gt(new BN(1)) || mint.info.decimals !== 0) { if (mint.info.supply.gt(new BN(1)) || mint.info.decimals !== 0) {
// naive not NFT check // naive not NFT check
mintToMetadata.delete(key); delete mintToMetadata[key];
} else { } else {
const metadata = mintToMetadata.get(key); const metadata = mintToMetadata[key];
if (metadata && metadata.info.uri) { if (metadata && metadata.info.uri) {
extendedMetadataFetch.set( extendedMetadataFetch.set(
key, key,
@ -190,19 +398,19 @@ const queryExtendedMetadata = async (
!metadata.info.extended || !metadata.info.extended ||
metadata.info.extended?.files?.length === 0 metadata.info.extended?.files?.length === 0
) { ) {
mintToMetadata.delete(key); delete mintToMetadata[key];
} else { } else {
if (metadata.info.extended?.image) { if (metadata.info.extended?.image) {
metadata.info.extended.image = `${metadata.info.uri}/${metadata.info.extended.image}`; metadata.info.extended.image = `${metadata.info.uri}/${metadata.info.extended.image}`;
} }
} }
} catch { } catch {
mintToMetadata.delete(key); delete mintToMetadata[key];
return undefined; return undefined;
} }
}) })
.catch(() => { .catch(() => {
mintToMetadata.delete(key); delete mintToMetadata[key];
return undefined; return undefined;
}), }),
); );
@ -212,7 +420,7 @@ const queryExtendedMetadata = async (
await Promise.all([...extendedMetadataFetch.values()]); await Promise.all([...extendedMetadataFetch.values()]);
setMetadata([...mintToMetadata.values()]); setMetadata([...Object.values(mintToMetadata)]);
}; };
export const useMeta = () => { export const useMeta = () => {

View File

@ -1,6 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { PublicKey } from '@solana/web3.js'; import { PublicKey } from '@solana/web3.js';
import { useMeta } from './../contexts'; import { useMeta } from '../contexts';
import { Art } from '../types'; import { Art } from '../types';
export const useArt = (id: PublicKey | string) => { export const useArt = (id: PublicKey | string) => {

View File

@ -0,0 +1,45 @@
import {
ParsedAccount,
Metadata,
SafetyDepositBox,
AuctionData,
useConnection,
AuctionState,
} from '@oyster/common';
import { useEffect, useState } from 'react';
import { AuctionView, processAccountsIntoAuctionView } from '.';
import { useMeta } from '../contexts';
import { AuctionManager } from '../models/metaplex';
import { sampleAuction } from '../views/home/sampleData';
export const useAuction = (id: string) => {
const connection = useConnection();
const [clock, setClock] = useState<number>(0);
const [auctionView, setAuctionView] = useState<AuctionView | null>(null);
useEffect(() => {
connection.getSlot().then(setClock);
}, [connection]);
const {
auctions,
auctionManagers,
safetyDepositBoxesByVaultAndIndex,
metadataByMint,
} = useMeta();
useEffect(() => {
const auction = auctions[id];
if (auction) {
const auctionView = processAccountsIntoAuctionView(
auction,
auctionManagers,
safetyDepositBoxesByVaultAndIndex,
metadataByMint,
clock,
undefined,
);
if (auctionView) setAuctionView(auctionView);
}
}, [clock]);
return auctionView;
};

View File

@ -1,9 +0,0 @@
import React, { useMemo } from 'react';
import { sampleAuction } from '../views/home/sampleData';
import { useMeta } from './../contexts';
export const useAuction = (id: string) => {
const { metadata } = useMeta();
return sampleAuction;
}

View File

@ -0,0 +1,155 @@
import {
ParsedAccount,
Metadata,
SafetyDepositBox,
AuctionData,
useConnection,
AuctionState,
} from '@oyster/common';
import { useEffect, useState } from 'react';
import { useMeta } from '../contexts';
import { AuctionManager } from '../models/metaplex';
export enum AuctionViewState {
Live = '0',
Upcoming = '1',
Ended = '2',
BuyNow = '3',
}
export interface AuctionViewItem {
metadata: ParsedAccount<Metadata>;
safetyDeposit: ParsedAccount<SafetyDepositBox>;
}
// Flattened surface item for easy display
export interface AuctionView {
items: AuctionViewItem[];
auction: ParsedAccount<AuctionData>;
auctionManager: ParsedAccount<AuctionManager>;
openEditionItem?: AuctionViewItem;
state: AuctionViewState;
thumbnail: AuctionViewItem;
}
export const useAuctions = (state: AuctionViewState) => {
const connection = useConnection();
const [clock, setClock] = useState<number>(0);
const [auctionViews, setAuctionViews] = useState<AuctionView[]>([]);
useEffect(() => {
connection.getSlot().then(setClock);
}, [connection]);
const {
auctions,
auctionManagers,
safetyDepositBoxesByVaultAndIndex,
metadataByMint,
} = useMeta();
useEffect(() => {
const newAuctionViews: AuctionView[] = [];
Object.keys(auctions).forEach(a => {
const auction = auctions[a];
const auctionView = processAccountsIntoAuctionView(
auction,
auctionManagers,
safetyDepositBoxesByVaultAndIndex,
metadataByMint,
clock,
state,
);
if (auctionView) newAuctionViews.push(auctionView);
});
setAuctionViews(newAuctionViews);
}, [clock, state, auctions]);
return auctionViews;
};
export function processAccountsIntoAuctionView(
auction: ParsedAccount<AuctionData>,
auctionManagers: Record<string, ParsedAccount<AuctionManager>>,
safetyDepositBoxesByVaultAndIndex: Record<
string,
ParsedAccount<SafetyDepositBox>
>,
metadataByMint: Record<string, ParsedAccount<Metadata>>,
clock: number,
desiredState: AuctionViewState | undefined,
) {
let state: AuctionViewState;
if (
auction.info.state == AuctionState.Ended ||
(auction.info.endedAt && auction.info.endedAt.toNumber() <= clock)
) {
state = AuctionViewState.Ended;
} else if (
auction.info.state == AuctionState.Started ||
(auction.info.endedAt && auction.info.endedAt.toNumber() > clock)
) {
state = AuctionViewState.Live;
} else if (auction.info.state == AuctionState.Created) {
state = AuctionViewState.Upcoming;
} else {
state = AuctionViewState.BuyNow;
}
if (desiredState && desiredState != state) return null;
const auctionManager =
auctionManagers[auction.info.auctionManagerKey?.toBase58() || ''];
if (auctionManager) {
let boxes: ParsedAccount<SafetyDepositBox>[] = [];
let box =
safetyDepositBoxesByVaultAndIndex[
auctionManager.info.vault.toBase58() + '-0'
];
if (box) {
boxes.push(box);
let i = 1;
while (box) {
box =
safetyDepositBoxesByVaultAndIndex[
auctionManager.info.vault.toBase58() + '-' + i.toString()
];
if (box) boxes.push(box);
i++;
}
}
if (boxes.length > 0) {
let view: any = {
auction,
auctionManager,
state,
items: auctionManager.info.settings.winningConfigs.map(w => ({
metadata:
metadataByMint[
boxes[w.safetyDepositBoxIndex].info.tokenMint.toBase58()
],
safetyDeposit: boxes[w.safetyDepositBoxIndex],
})),
openEditionItem:
auctionManager.info.settings.openEditionConfig != null
? {
metadata:
metadataByMint[
boxes[
auctionManager.info.settings.openEditionConfig
].info.tokenMint.toBase58()
],
safetyDeposit:
boxes[auctionManager.info.settings.openEditionConfig],
}
: undefined,
};
view.thumbnail = view.items[0] || view.openEditionItem;
if (!view.thumbnail || !view.thumbnail.metadata) return null;
return view;
}
}
return null;
}

View File

@ -1,15 +0,0 @@
import React, { useMemo } from 'react';
import { useMeta } from './../contexts';
export enum AuctionState {
Live = '0',
Upcoming = '1',
Ended = '2',
BuyNow = '3',
}
export const useAuctions = (state: AuctionState) => {
const { metadata } = useMeta();
return metadata;
}

View File

@ -140,6 +140,9 @@ export class WinningConfig {
Object.assign(this, args); Object.assign(this, args);
} }
} }
export const decodeAuctionManager = (buffer: Buffer) => {
return deserializeBorsh(SCHEMA, AuctionManager, buffer) as AuctionManager;
};
export class WinningConfigState { export class WinningConfigState {
amountMinted: number = 0; amountMinted: number = 0;
@ -313,6 +316,20 @@ export const SCHEMA = new Map<any, any>([
], ],
]); ]);
export async function getAuctionManagerKey(
vault: PublicKey,
auctionKey: PublicKey,
): Promise<PublicKey> {
const PROGRAM_IDS = programIds();
return (
await PublicKey.findProgramAddress(
[Buffer.from(METAPLEX_PREFIX), auctionKey.toBuffer()],
PROGRAM_IDS.metaplex,
)
)[0];
}
export async function getAuctionKeys( export async function getAuctionKeys(
vault: PublicKey, vault: PublicKey,
): Promise<{ auctionKey: PublicKey; auctionManagerKey: PublicKey }> { ): Promise<{ auctionKey: PublicKey; auctionManagerKey: PublicKey }> {
@ -329,12 +346,7 @@ export async function getAuctionKeys(
) )
)[0]; )[0];
const auctionManagerKey: PublicKey = ( const auctionManagerKey = await getAuctionManagerKey(vault, auctionKey);
await PublicKey.findProgramAddress(
[Buffer.from(METAPLEX_PREFIX), auctionKey.toBuffer()],
PROGRAM_IDS.metaplex,
)
)[0];
return { auctionKey, auctionManagerKey }; return { auctionKey, auctionManagerKey };
} }

View File

@ -1,32 +1,40 @@
import React from 'react'; import React from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { Row, Col, Divider, Layout, Image } from 'antd'; import { Row, Col, Divider, Layout, Image, Spin } from 'antd';
import { AuctionCard } from '../../components/AuctionCard'; import { AuctionCard } from '../../components/AuctionCard';
import { useArt, useAuction } from '../../hooks'; import { useArt, useAuction } from '../../hooks';
import { ArtContent } from '../../components/ArtContent'; import { ArtContent } from '../../components/ArtContent';
import { sampleArtist } from '../home/sampleData'; import { sampleArtist } from '../home/sampleData';
const { Content } = Layout const { Content } = Layout;
export const AuctionView = () => { export const AuctionView = () => {
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const auction = useAuction(id); const auction = useAuction(id);
const art = useArt(id); const art = useArt(id);
const artist = sampleArtist const artist = sampleArtist;
return ( return (
<Content> <Content>
<Col> <Col>
<Row> <Row>
<ArtContent category={art.category} content={art.image} className="artwork-image" /> <ArtContent
category={art.category}
content={art.image}
className="artwork-image"
/>
</Row> </Row>
<Divider /> <Divider />
<Row style={{ margin: '0 30px', textAlign: 'left', fontSize: '1.4rem' }}> <Row
style={{ margin: '0 30px', textAlign: 'left', fontSize: '1.4rem' }}
>
<Col span={12}> <Col span={12}>
<div style={{ fontWeight: 700 }}>{art.title}</div> <div style={{ fontWeight: 700 }}>{art.title}</div>
<br /> <br />
<div className="info-header">CREATED BY</div> <div className="info-header">CREATED BY</div>
<div className="info-content"><img src={artist.image} className="artist-image" /> @{art.artist}</div> <div className="info-content">
<img src={artist.image} className="artist-image" /> @{art.artist}
</div>
<br /> <br />
<div className="info-header">CREATOR ROYALTIES</div> <div className="info-header">CREATOR ROYALTIES</div>
<div className="royalties">{art.royalties}%</div> <div className="royalties">{art.royalties}%</div>
@ -38,7 +46,7 @@ export const AuctionView = () => {
<div className="info-content">{artist.about}</div> <div className="info-content">{artist.about}</div>
</Col> </Col>
<Col span={12}> <Col span={12}>
<AuctionCard auction={auction} /> {auction ? <AuctionCard auctionView={auction} /> : <Spin />}
</Col> </Col>
</Row> </Row>
</Col> </Col>

View File

@ -126,7 +126,7 @@ export const AuctionCreateView = () => {
const [attributes, setAttributes] = useState<AuctionState>({ const [attributes, setAttributes] = useState<AuctionState>({
reservationPrice: 0, reservationPrice: 0,
items: [], items: [],
category: AuctionCategory.Open, category: AuctionCategory.Single,
saleType: 'auction', saleType: 'auction',
}); });
@ -157,6 +157,7 @@ export const AuctionCreateView = () => {
usize: new BN(0), usize: new BN(0),
}); });
} else if (attributes.category == AuctionCategory.Single) { } else if (attributes.category == AuctionCategory.Single) {
console.log('Using single');
settings = new AuctionManagerSettings({ settings = new AuctionManagerSettings({
openEditionWinnerConstraint: WinningConstraint.NoOpenEdition, openEditionWinnerConstraint: WinningConstraint.NoOpenEdition,
openEditionNonWinningConstraint: NonWinningConstraint.NoOpenEdition, openEditionNonWinningConstraint: NonWinningConstraint.NoOpenEdition,

View File

@ -1,52 +1,58 @@
import React, { useState } from 'react' import React, { useState } from 'react';
import { Layout, Row, Col, Tabs } from 'antd' import { Layout, Row, Col, Tabs } from 'antd';
import Masonry from 'react-masonry-css' import Masonry from 'react-masonry-css';
import { PreSaleBanner } from '../../components/PreSaleBanner' import { PreSaleBanner } from '../../components/PreSaleBanner';
import { AuctionState, useAuctions } from '../../hooks' import { AuctionViewState, useAuctions } from '../../hooks';
import './index.less' import './index.less';
import { ArtCard } from '../../components/ArtCard' import { ArtCard } from '../../components/ArtCard';
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom';
const { TabPane } = Tabs; const { TabPane } = Tabs;
const { Content } = Layout const { Content } = Layout;
export const HomeView = () => { export const HomeView = () => {
const [activeKey, setActiveKey] = useState(AuctionState.Live); const [activeKey, setActiveKey] = useState(AuctionViewState.Live);
const auctions = useAuctions(AuctionState.Live) const auctions = useAuctions(activeKey);
console.log('Auctions', auctions);
const breakpointColumnsObj = { const breakpointColumnsObj = {
default: 4, default: 4,
1100: 3, 1100: 3,
700: 2, 700: 2,
500: 1 500: 1,
} };
const auctionGrid = <Masonry const auctionGrid = (
<Masonry
breakpointCols={breakpointColumnsObj} breakpointCols={breakpointColumnsObj}
className="my-masonry-grid" className="my-masonry-grid"
columnClassName="my-masonry-grid_column" columnClassName="my-masonry-grid_column"
> >
{auctions.map(m => { {auctions.map(m => {
const id = m.pubkey.toBase58(); const id = m.auction.pubkey.toBase58();
return <Link to={`/art/${id}`}> return (
<ArtCard key={id} <Link to={`/art/${id}`}>
image={m.info.extended?.image} <ArtCard
category={m.info.extended?.category} key={id}
name={m.info?.name} image={m.thumbnail.metadata.info.extended?.image}
symbol={m.info.symbol} category={m.thumbnail.metadata.info.extended?.category}
description={m.info.extended?.description} name={m.thumbnail.metadata?.info.name}
preview={false} /> symbol={m.thumbnail.metadata?.info.symbol}
description={m.thumbnail.metadata?.info.extended?.description}
preview={false}
/>
</Link> </Link>
);
})} })}
</Masonry>; </Masonry>
);
return ( return (
<Layout style={{ margin: 0, marginTop: 30 }}> <Layout style={{ margin: 0, marginTop: 30 }}>
<PreSaleBanner <PreSaleBanner
artistName={"RAC"} artistName={'RAC'}
productName={"THE BOY COLLECTION"} productName={'THE BOY COLLECTION'}
preSaleTS={1618690343000} preSaleTS={1618690343000}
image="img/banner1.jpeg" image="img/banner1.jpeg"
/> />
@ -54,28 +60,31 @@ export const HomeView = () => {
<Content style={{ display: 'flex', flexWrap: 'wrap' }}> <Content style={{ display: 'flex', flexWrap: 'wrap' }}>
<Col style={{ width: '100%', marginTop: 10 }}> <Col style={{ width: '100%', marginTop: 10 }}>
<Row> <Row>
<Tabs activeKey={activeKey} onTabClick={(key) => setActiveKey(key as AuctionState)}> <Tabs
activeKey={activeKey}
onTabClick={key => setActiveKey(key as AuctionViewState)}
>
<TabPane <TabPane
tab={<span className="tab-title">Live</span>} tab={<span className="tab-title">Live</span>}
key={AuctionState.Live} key={AuctionViewState.Live}
> >
{auctionGrid} {auctionGrid}
</TabPane> </TabPane>
<TabPane <TabPane
tab={<span className="tab-title">Upcoming</span>} tab={<span className="tab-title">Upcoming</span>}
key={AuctionState.Upcoming} key={AuctionViewState.Upcoming}
> >
{auctionGrid} {auctionGrid}
</TabPane> </TabPane>
<TabPane <TabPane
tab={<span className="tab-title">Ended</span>} tab={<span className="tab-title">Ended</span>}
key={AuctionState.Ended} key={AuctionViewState.Ended}
> >
{auctionGrid} {auctionGrid}
</TabPane> </TabPane>
<TabPane <TabPane
tab={<span className="tab-title">Buy Now</span>} tab={<span className="tab-title">Buy Now</span>}
key={AuctionState.BuyNow} key={AuctionViewState.BuyNow}
> >
{auctionGrid} {auctionGrid}
</TabPane> </TabPane>