Merge remote-tracking branch 'origin/master' into feature/video

This commit is contained in:
bartosz-lipinski 2021-06-27 20:56:48 -05:00
commit 3be750cb69
19 changed files with 1716 additions and 383 deletions

View File

@ -15,6 +15,7 @@ import { findProgramAddress } from '../utils';
export const AUCTION_PREFIX = 'auction'; export const AUCTION_PREFIX = 'auction';
export const METADATA = 'metadata'; export const METADATA = 'metadata';
export const EXTENDED = 'extended'; export const EXTENDED = 'extended';
export const MAX_AUCTION_DATA_EXTENDED_SIZE = 8 + 9 + 2 + 200;
export enum AuctionState { export enum AuctionState {
Created = 0, Created = 0,
@ -91,6 +92,23 @@ export const decodeBidderPot = (buffer: Buffer) => {
return deserializeUnchecked(AUCTION_SCHEMA, BidderPot, buffer) as BidderPot; return deserializeUnchecked(AUCTION_SCHEMA, BidderPot, buffer) as BidderPot;
}; };
export const AuctionDataExtendedParser: AccountParser = (
pubkey: PublicKey,
account: AccountInfo<Buffer>,
) => ({
pubkey,
account,
info: decodeAuctionDataExtended(account.data),
});
export const decodeAuctionDataExtended = (buffer: Buffer) => {
return deserializeUnchecked(
AUCTION_SCHEMA,
AuctionDataExtended,
buffer,
) as AuctionDataExtended;
};
export const BidderMetadataParser: AccountParser = ( export const BidderMetadataParser: AccountParser = (
pubkey: PublicKey, pubkey: PublicKey,
account: AccountInfo<Buffer>, account: AccountInfo<Buffer>,
@ -323,7 +341,24 @@ export class WinnerLimit {
} }
} }
class CreateAuctionArgs { export interface IPartialCreateAuctionArgs {
/// How many winners are allowed for this auction. See AuctionData.
winners: WinnerLimit;
/// End time is the cut-off point that the auction is forced to end by. See AuctionData.
endAuctionAt: BN | null;
/// Gap time is how much time after the previous bid where the auction ends. See AuctionData.
auctionGap: BN | null;
/// Token mint for the SPL token used for bidding.
tokenMint: PublicKey;
priceFloor: PriceFloor;
tickSize: BN | null;
gapTickSizePercentage: number | null;
}
export class CreateAuctionArgs implements IPartialCreateAuctionArgs {
instruction: number = 1; instruction: number = 1;
/// How many winners are allowed for this auction. See AuctionData. /// How many winners are allowed for this auction. See AuctionData.
winners: WinnerLimit; winners: WinnerLimit;
@ -340,6 +375,10 @@ class CreateAuctionArgs {
priceFloor: PriceFloor; priceFloor: PriceFloor;
tickSize: BN | null;
gapTickSizePercentage: number | null;
constructor(args: { constructor(args: {
winners: WinnerLimit; winners: WinnerLimit;
endAuctionAt: BN | null; endAuctionAt: BN | null;
@ -348,6 +387,8 @@ class CreateAuctionArgs {
authority: PublicKey; authority: PublicKey;
resource: PublicKey; resource: PublicKey;
priceFloor: PriceFloor; priceFloor: PriceFloor;
tickSize: BN | null;
gapTickSizePercentage: number | null;
}) { }) {
this.winners = args.winners; this.winners = args.winners;
this.endAuctionAt = args.endAuctionAt; this.endAuctionAt = args.endAuctionAt;
@ -356,6 +397,8 @@ class CreateAuctionArgs {
this.authority = args.authority; this.authority = args.authority;
this.resource = args.resource; this.resource = args.resource;
this.priceFloor = args.priceFloor; this.priceFloor = args.priceFloor;
this.tickSize = args.tickSize;
this.gapTickSizePercentage = args.gapTickSizePercentage;
} }
} }
@ -402,6 +445,8 @@ export const AUCTION_SCHEMA = new Map<any, any>([
['authority', 'pubkey'], ['authority', 'pubkey'],
['resource', 'pubkey'], ['resource', 'pubkey'],
['priceFloor', PriceFloor], ['priceFloor', PriceFloor],
['tickSize', { kind: 'option', type: 'u64' }],
['gapTickSizePercentage', { kind: 'option', type: 'u8' }],
], ],
}, },
], ],
@ -541,39 +586,20 @@ export const decodeAuctionData = (buffer: Buffer) => {
}; };
export async function createAuction( export async function createAuction(
winners: WinnerLimit, settings: CreateAuctionArgs,
resource: PublicKey,
endAuctionAt: BN | null,
auctionGap: BN | null,
priceFloor: PriceFloor,
tokenMint: PublicKey,
authority: PublicKey,
creator: PublicKey, creator: PublicKey,
instructions: TransactionInstruction[], instructions: TransactionInstruction[],
) { ) {
const auctionProgramId = programIds().auction; const auctionProgramId = programIds().auction;
const data = Buffer.from( const data = Buffer.from(serialize(AUCTION_SCHEMA, settings));
serialize(
AUCTION_SCHEMA,
new CreateAuctionArgs({
winners,
resource,
endAuctionAt,
auctionGap,
tokenMint,
authority,
priceFloor,
}),
),
);
const auctionKey: PublicKey = ( const auctionKey: PublicKey = (
await findProgramAddress( await findProgramAddress(
[ [
Buffer.from(AUCTION_PREFIX), Buffer.from(AUCTION_PREFIX),
auctionProgramId.toBuffer(), auctionProgramId.toBuffer(),
resource.toBuffer(), settings.resource.toBuffer(),
], ],
auctionProgramId, auctionProgramId,
) )
@ -591,7 +617,10 @@ export async function createAuction(
isWritable: true, isWritable: true,
}, },
{ {
pubkey: await getAuctionExtended({ auctionProgramId, resource }), pubkey: await getAuctionExtended({
auctionProgramId,
resource: settings.resource,
}),
isSigner: false, isSigner: false,
isWritable: true, isWritable: true,
}, },

View File

@ -154,17 +154,20 @@ export class ReservationList {
/// What supply counter was on master_edition when this reservation was created. /// What supply counter was on master_edition when this reservation was created.
supplySnapshot: BN | null; supplySnapshot: BN | null;
reservations: Reservation[]; reservations: Reservation[];
totalReservationSpots: BN;
constructor(args: { constructor(args: {
key: MetadataKey; key: MetadataKey;
masterEdition: PublicKey; masterEdition: PublicKey;
supplySnapshot: BN | null; supplySnapshot: BN | null;
reservations: Reservation[]; reservations: Reservation[];
totalReservationSpots: BN;
}) { }) {
this.key = MetadataKey.EditionV1; this.key = MetadataKey.EditionV1;
this.masterEdition = args.masterEdition; this.masterEdition = args.masterEdition;
this.supplySnapshot = args.supplySnapshot; this.supplySnapshot = args.supplySnapshot;
this.reservations = args.reservations; this.reservations = args.reservations;
this.totalReservationSpots = args.totalReservationSpots;
} }
} }
@ -408,6 +411,7 @@ export const METADATA_SCHEMA = new Map<any, any>([
['masterEdition', 'pubkey'], ['masterEdition', 'pubkey'],
['supplySnapshot', { kind: 'option', type: 'u64' }], ['supplySnapshot', { kind: 'option', type: 'u64' }],
['reservations', [Reservation]], ['reservations', [Reservation]],
['totalReservationSpots', 'u64'],
], ],
}, },
], ],

View File

@ -8,7 +8,6 @@ import {
actions, actions,
Metadata, Metadata,
ParsedAccount, ParsedAccount,
WinnerLimit,
MasterEdition, MasterEdition,
SequenceType, SequenceType,
sendTransactions, sendTransactions,
@ -20,8 +19,8 @@ import {
getSafetyDepositBoxAddress, getSafetyDepositBoxAddress,
createAssociatedTokenAccountInstruction, createAssociatedTokenAccountInstruction,
sendTransactionWithRetry, sendTransactionWithRetry,
PriceFloor,
findProgramAddress, findProgramAddress,
IPartialCreateAuctionArgs,
} from '@oyster/common'; } from '@oyster/common';
import { AccountLayout, Token } from '@solana/spl-token'; import { AccountLayout, Token } from '@solana/spl-token';
@ -100,13 +99,10 @@ export async function createAuctionManager(
ParsedAccount<WhitelistedCreator> ParsedAccount<WhitelistedCreator>
>, >,
settings: AuctionManagerSettings, settings: AuctionManagerSettings,
winnerLimit: WinnerLimit, auctionSettings: IPartialCreateAuctionArgs,
endAuctionAt: BN,
auctionGap: BN,
safetyDepositDrafts: SafetyDepositDraft[], safetyDepositDrafts: SafetyDepositDraft[],
participationSafetyDepositDraft: SafetyDepositDraft | undefined, participationSafetyDepositDraft: SafetyDepositDraft | undefined,
paymentMint: PublicKey, paymentMint: PublicKey,
priceFloor: PriceFloor,
): Promise<{ ): Promise<{
vault: PublicKey; vault: PublicKey;
auction: PublicKey; auction: PublicKey;
@ -136,15 +132,7 @@ export async function createAuctionManager(
instructions: makeAuctionInstructions, instructions: makeAuctionInstructions,
signers: makeAuctionSigners, signers: makeAuctionSigners,
auction, auction,
} = await makeAuction( } = await makeAuction(wallet, vault, auctionSettings);
wallet,
winnerLimit,
vault,
endAuctionAt,
auctionGap,
paymentMint,
priceFloor,
);
let safetyDepositConfigsWithPotentiallyUnsetTokens = let safetyDepositConfigsWithPotentiallyUnsetTokens =
await buildSafetyDepositArray( await buildSafetyDepositArray(

View File

@ -2,24 +2,19 @@ import { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js';
import { import {
utils, utils,
actions, actions,
WinnerLimit,
PriceFloor,
findProgramAddress, findProgramAddress,
IPartialCreateAuctionArgs,
CreateAuctionArgs,
} from '@oyster/common'; } from '@oyster/common';
import BN from 'bn.js';
import { METAPLEX_PREFIX } from '../models/metaplex'; import { METAPLEX_PREFIX } from '../models/metaplex';
const { AUCTION_PREFIX, createAuction } = actions; const { AUCTION_PREFIX, createAuction } = actions;
// This command makes an auction // This command makes an auction
export async function makeAuction( export async function makeAuction(
wallet: any, wallet: any,
winnerLimit: WinnerLimit,
vault: PublicKey, vault: PublicKey,
endAuctionAt: BN, auctionSettings: IPartialCreateAuctionArgs,
auctionGap: BN,
paymentMint: PublicKey,
priceFloor: PriceFloor,
): Promise<{ ): Promise<{
auction: PublicKey; auction: PublicKey;
instructions: TransactionInstruction[]; instructions: TransactionInstruction[];
@ -47,17 +42,13 @@ export async function makeAuction(
) )
)[0]; )[0];
createAuction( const fullSettings = new CreateAuctionArgs({
winnerLimit, ...auctionSettings,
vault, authority: auctionManagerKey,
endAuctionAt, resource: vault,
auctionGap, });
priceFloor,
paymentMint, createAuction(fullSettings, wallet.publicKey, instructions);
auctionManagerKey,
wallet.publicKey,
instructions,
);
return { instructions, signers, auction: auctionKey }; return { instructions, signers, auction: auctionKey };
} }

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Col, Button, InputNumber, Spin } from 'antd'; import { Col, Button, InputNumber, Spin } from 'antd';
import { MemoryRouter, Route, Redirect, Link } from 'react-router-dom'; import { MemoryRouter, Route, Redirect, Link } from 'react-router-dom';
@ -13,6 +13,10 @@ import {
formatTokenAmount, formatTokenAmount,
useMint, useMint,
PriceFloorType, PriceFloorType,
AuctionDataExtended,
ParsedAccount,
getAuctionExtended,
programIds,
} from '@oyster/common'; } from '@oyster/common';
import { AuctionView, useUserBalance } from '../../hooks'; import { AuctionView, useUserBalance } from '../../hooks';
import { sendPlaceBid } from '../../actions/sendPlaceBid'; import { sendPlaceBid } from '../../actions/sendPlaceBid';
@ -26,9 +30,78 @@ import BN from 'bn.js';
import { Confetti } from '../Confetti'; import { Confetti } from '../Confetti';
import { QUOTE_MINT } from '../../constants'; import { QUOTE_MINT } from '../../constants';
import { LAMPORTS_PER_SOL } from '@solana/web3.js'; import { LAMPORTS_PER_SOL } from '@solana/web3.js';
import { useMeta } from '../../contexts';
import moment from 'moment';
const { useWallet } = contexts.Wallet; const { useWallet } = contexts.Wallet;
function useGapTickCheck(
value: number | undefined,
gapTick: number | null,
gapTime: number,
auctionView: AuctionView,
): boolean {
return !!useMemo(() => {
if (gapTick && value && gapTime && !auctionView.auction.info.ended()) {
// so we have a gap tick percentage, and a gap tick time, and a value, and we're not ended - are we within gap time?
const now = moment().unix();
const endedAt = auctionView.auction.info.endedAt;
if (endedAt) {
const ended = endedAt.toNumber();
if (now > ended) {
const toLamportVal = value * LAMPORTS_PER_SOL;
// Ok, we are in gap time, since now is greater than ended and we're not actually an ended auction yt.
// Check that the bid is at least gapTick % bigger than the next biggest one in the stack.
for (
let i = auctionView.auction.info.bidState.bids.length - 1;
i > -1;
i--
) {
const bid = auctionView.auction.info.bidState.bids[i];
const expected = bid.amount.toNumber();
if (expected < toLamportVal) {
const higherExpectedAmount = expected * ((100 + gapTick) / 100);
return higherExpectedAmount > toLamportVal;
} else if (expected == toLamportVal) {
// If gap tick is set, no way you can bid in this case - you must bid higher.
return true;
}
}
return false;
} else {
return false;
}
}
return false;
}
}, [value, gapTick, gapTime, auctionView]);
}
function useAuctionExtended(
auctionView: AuctionView,
): ParsedAccount<AuctionDataExtended> | undefined {
const [auctionExtended, setAuctionExtended] =
useState<ParsedAccount<AuctionDataExtended>>();
const { auctionDataExtended } = useMeta();
useMemo(() => {
const fn = async () => {
if (!auctionExtended) {
const PROGRAM_IDS = programIds();
const extendedKey = await getAuctionExtended({
auctionProgramId: PROGRAM_IDS.auction,
resource: auctionView.vault.pubkey,
});
const extendedValue = auctionDataExtended[extendedKey.toBase58()];
if (extendedValue) setAuctionExtended(extendedValue);
}
};
fn();
}, [auctionDataExtended, auctionExtended, setAuctionExtended]);
return auctionExtended;
}
export const AuctionCard = ({ export const AuctionCard = ({
auctionView, auctionView,
style, style,
@ -74,9 +147,21 @@ export const AuctionCard = ({
winnerIndex, winnerIndex,
auctionView, auctionView,
); );
const auctionExtended = useAuctionExtended(auctionView);
const eligibleForAnything = winnerIndex !== null || eligibleForOpenEdition; const eligibleForAnything = winnerIndex !== null || eligibleForOpenEdition;
const gapTime = (auctionView.auction.info.auctionGap?.toNumber() || 0) / 60; const gapTime = (auctionView.auction.info.auctionGap?.toNumber() || 0) / 60;
const gapTick = auctionExtended
? auctionExtended.info.gapTickSizePercentage
: 0;
const tickSize = auctionExtended ? auctionExtended.info.tickSize : 0;
const tickSizeInvalid = !!(
tickSize &&
value &&
(value * LAMPORTS_PER_SOL) % tickSize.toNumber() != 0
);
const gapBidInvalid = useGapTickCheck(value, gapTick, gapTime, auctionView);
return ( return (
<div className="auction-container" style={style}> <div className="auction-container" style={style}>
@ -270,13 +355,32 @@ export const AuctionCard = ({
}} }}
> >
Bids placed in the last {gapTime} minutes will extend Bids placed in the last {gapTime} minutes will extend
bidding for another {gapTime} minutes. bidding for another {gapTime} minutes beyond the point in
time that bid was made.{' '}
{gapTick && (
<span>
Additionally, once the official auction end time has
passed, only bids {gapTick}% larger than an existing
bid will be accepted.
</span>
)}
</div> </div>
)} )}
<br /> <br />
<AuctionNumbers auctionView={auctionView} /> <AuctionNumbers auctionView={auctionView} />
<br /> <br />
{tickSizeInvalid && tickSize && (
<span style={{ color: 'red' }}>
Tick size is {tickSize.toNumber() / LAMPORTS_PER_SOL}.
</span>
)}
{gapBidInvalid && (
<span style={{ color: 'red' }}>
Your bid needs to be at least {gapTick}% larger than an
existing bid during gap periods to be eligible.
</span>
)}
<div <div
style={{ style={{
@ -333,6 +437,8 @@ export const AuctionCard = ({
className="action-btn" className="action-btn"
onClick={placeBid} onClick={placeBid}
disabled={ disabled={
tickSizeInvalid ||
gapBidInvalid ||
!myPayingAccount || !myPayingAccount ||
value === undefined || value === undefined ||
value * LAMPORTS_PER_SOL < priceFloor || value * LAMPORTS_PER_SOL < priceFloor ||

View File

@ -27,6 +27,9 @@ import {
Vault, Vault,
setProgramIds, setProgramIds,
useConnectionConfig, useConnectionConfig,
AuctionDataExtended,
MAX_AUCTION_DATA_EXTENDED_SIZE,
AuctionDataExtendedParser,
} 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';
@ -66,6 +69,7 @@ interface MetaState {
masterEditionsByOneTimeAuthMint: Record<string, ParsedAccount<MasterEdition>>; masterEditionsByOneTimeAuthMint: Record<string, ParsedAccount<MasterEdition>>;
auctionManagersByAuction: Record<string, ParsedAccount<AuctionManager>>; auctionManagersByAuction: Record<string, ParsedAccount<AuctionManager>>;
auctions: Record<string, ParsedAccount<AuctionData>>; auctions: Record<string, ParsedAccount<AuctionData>>;
auctionDataExtended: Record<string, ParsedAccount<AuctionDataExtended>>;
vaults: Record<string, ParsedAccount<Vault>>; vaults: Record<string, ParsedAccount<Vault>>;
store: ParsedAccount<Store> | null; store: ParsedAccount<Store> | null;
bidderMetadataByAuctionAndBidder: Record< bidderMetadataByAuctionAndBidder: Record<
@ -87,29 +91,38 @@ interface MetaState {
const { MetadataKey } = actions; const { MetadataKey } = actions;
type UpdateStateValueFunc = (prop: keyof MetaState, key: string, value: any) => void; type UpdateStateValueFunc = (
prop: keyof MetaState,
key: string,
value: any,
) => void;
export interface MetaContextState extends MetaState { export interface MetaContextState extends MetaState {
isLoading: boolean; isLoading: boolean;
} }
const isMetadataPartOfStore = (m: ParsedAccount<Metadata> , store: ParsedAccount<Store> | null, whitelistedCreatorsByCreator: Record< const isMetadataPartOfStore = (
string, m: ParsedAccount<Metadata>,
ParsedAccount<WhitelistedCreator> store: ParsedAccount<Store> | null,
>) => { whitelistedCreatorsByCreator: Record<
if(!m?.info?.data?.creators) { string,
ParsedAccount<WhitelistedCreator>
>,
) => {
if (!m?.info?.data?.creators) {
return false; return false;
} }
return m.info.data.creators.findIndex( return (
m.info.data.creators.findIndex(
c => c =>
c.verified && c.verified &&
store && store &&
store.info && store.info &&
(store.info.public || (store.info.public ||
whitelistedCreatorsByCreator[c.address.toBase58()]?.info whitelistedCreatorsByCreator[c.address.toBase58()]?.info?.activated),
?.activated), ) >= 0
) >= 0; );
} };
const MetaContext = React.createContext<MetaContextState>({ const MetaContext = React.createContext<MetaContextState>({
metadata: [], metadata: [],
@ -121,6 +134,7 @@ const MetaContext = React.createContext<MetaContextState>({
editions: {}, editions: {},
auctionManagersByAuction: {}, auctionManagersByAuction: {},
auctions: {}, auctions: {},
auctionDataExtended: {},
vaults: {}, vaults: {},
store: null, store: null,
isLoading: false, isLoading: false,
@ -140,13 +154,20 @@ export function MetaProvider({ children = null as any }) {
metadata: [] as Array<ParsedAccount<Metadata>>, metadata: [] as Array<ParsedAccount<Metadata>>,
metadataByMint: {} as Record<string, ParsedAccount<Metadata>>, metadataByMint: {} as Record<string, ParsedAccount<Metadata>>,
masterEditions: {} as Record<string, ParsedAccount<MasterEdition>>, masterEditions: {} as Record<string, ParsedAccount<MasterEdition>>,
masterEditionsByPrintingMint: {} as Record<string, ParsedAccount<MasterEdition>>, masterEditionsByPrintingMint: {} as Record<
masterEditionsByOneTimeAuthMint: {} as Record<string, ParsedAccount<MasterEdition>>, string,
ParsedAccount<MasterEdition>
>,
masterEditionsByOneTimeAuthMint: {} as Record<
string,
ParsedAccount<MasterEdition>
>,
metadataByMasterEdition: {} as any, metadataByMasterEdition: {} as any,
editions: {}, editions: {},
auctionManagersByAuction: {}, auctionManagersByAuction: {},
bidRedemptions: {}, bidRedemptions: {},
auctions: {}, auctions: {},
auctionDataExtended: {},
vaults: {}, vaults: {},
payoutTickets: {}, payoutTickets: {},
store: null as ParsedAccount<Store> | null, store: null as ParsedAccount<Store> | null,
@ -154,7 +175,7 @@ export function MetaProvider({ children = null as any }) {
bidderMetadataByAuctionAndBidder: {}, bidderMetadataByAuctionAndBidder: {},
bidderPotsByAuctionAndBidder: {}, bidderPotsByAuctionAndBidder: {},
safetyDepositBoxesByVaultAndIndex: {}, safetyDepositBoxesByVaultAndIndex: {},
}) });
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -162,11 +183,11 @@ export function MetaProvider({ children = null as any }) {
async metadataByMint => { async metadataByMint => {
try { try {
const m = await queryExtendedMetadata(connection, metadataByMint); const m = await queryExtendedMetadata(connection, metadataByMint);
setState((current) => ({ setState(current => ({
...current, ...current,
metadata: m.metadata, metadata: m.metadata,
metadataByMint: m.mintToMetadata, metadataByMint: m.mintToMetadata,
})) }));
} catch (er) { } catch (er) {
console.error(er); console.error(er);
} }
@ -202,6 +223,7 @@ export function MetaProvider({ children = null as any }) {
auctionManagersByAuction: {}, auctionManagersByAuction: {},
bidRedemptions: {}, bidRedemptions: {},
auctions: {}, auctions: {},
auctionDataExtended: {},
vaults: {}, vaults: {},
payoutTickets: {}, payoutTickets: {},
store: null, store: null,
@ -214,11 +236,11 @@ export function MetaProvider({ children = null as any }) {
const updateTemp = (prop: keyof MetaState, key: string, value: any) => { const updateTemp = (prop: keyof MetaState, key: string, value: any) => {
if (prop === 'store') { if (prop === 'store') {
tempCache[prop] = value; tempCache[prop] = value;
} else if(tempCache[prop]) { } else if (tempCache[prop]) {
const bucket = tempCache[prop] as any; const bucket = tempCache[prop] as any;
bucket[key] = value as any; bucket[key] = value as any;
} }
} };
for (let i = 0; i < accounts.length; i++) { for (let i = 0; i < accounts.length; i++) {
let account = accounts[i]; let account = accounts[i];
@ -226,18 +248,25 @@ export function MetaProvider({ children = null as any }) {
processAuctions(account, updateTemp); processAuctions(account, updateTemp);
processMetaData(account, updateTemp); processMetaData(account, updateTemp);
await processMetaplexAccounts( await processMetaplexAccounts(account, updateTemp);
account,
updateTemp,
);
} }
const values = Object.values(tempCache.metadataByMint) as ParsedAccount<Metadata>[]; const values = Object.values(
tempCache.metadataByMint,
) as ParsedAccount<Metadata>[];
for (let i = 0; i < values.length; i++) { for (let i = 0; i < values.length; i++) {
const metadata = values[i]; const metadata = values[i];
if(isMetadataPartOfStore(metadata, tempCache.store, tempCache.whitelistedCreatorsByCreator)) { if (
isMetadataPartOfStore(
metadata,
tempCache.store,
tempCache.whitelistedCreatorsByCreator,
)
) {
await metadata.info.init(); await metadata.info.init();
tempCache.metadataByMasterEdition[metadata.info?.masterEdition?.toBase58() || ''] = metadata; tempCache.metadataByMasterEdition[
metadata.info?.masterEdition?.toBase58() || ''
] = metadata;
} else { } else {
delete tempCache.metadataByMint[metadata.info.mint.toBase58() || '']; delete tempCache.metadataByMint[metadata.info.mint.toBase58() || ''];
} }
@ -247,7 +276,7 @@ export function MetaProvider({ children = null as any }) {
tempCache.metadata = values; tempCache.metadata = values;
setState({ setState({
...tempCache, ...tempCache,
}) });
setIsLoading(false); setIsLoading(false);
console.log('------->set finished'); console.log('------->set finished');
@ -258,36 +287,34 @@ export function MetaProvider({ children = null as any }) {
return () => { return () => {
dispose(); dispose();
}; };
}, [ }, [connection, setState, updateMints, env]);
connection,
setState,
updateMints,
env,
]);
const updateStateValue = useMemo(() => (prop: keyof MetaState, key: string, value: any) => { const updateStateValue = useMemo(
setState((current) => { () => (prop: keyof MetaState, key: string, value: any) => {
if (prop === 'store') { setState(current => {
return { if (prop === 'store') {
...current, return {
[prop]: value, ...current,
[prop]: value,
};
} else {
return {
...current,
[prop]: {
...current[prop],
[key]: value,
},
};
} }
} else { });
return ({ },
...current, [setState],
[prop]: { );
...current[prop],
[key]: value
}
});
}
});
}, [setState]);
const store = state.store; const store = state.store;
const whitelistedCreatorsByCreator = state.whitelistedCreatorsByCreator; const whitelistedCreatorsByCreator = state.whitelistedCreatorsByCreator;
useEffect(() => { useEffect(() => {
if(isLoading) { if (isLoading) {
return; return;
} }
@ -340,7 +367,10 @@ export function MetaProvider({ children = null as any }) {
updateStateValue, updateStateValue,
); );
if(result && isMetadataPartOfStore(result, store, whitelistedCreatorsByCreator)) { if (
result &&
isMetadataPartOfStore(result, store, whitelistedCreatorsByCreator)
) {
await result.info.init(); await result.info.init();
setState((data) => ({ setState((data) => ({
...data, ...data,
@ -370,7 +400,7 @@ export function MetaProvider({ children = null as any }) {
pubkey, pubkey,
account: info.accountInfo, account: info.accountInfo,
}, },
updateStateValue updateStateValue,
); );
}, },
); );
@ -430,9 +460,12 @@ export function MetaProvider({ children = null as any }) {
masterEditions: state.masterEditions, masterEditions: state.masterEditions,
auctionManagersByAuction: state.auctionManagersByAuction, auctionManagersByAuction: state.auctionManagersByAuction,
auctions: state.auctions, auctions: state.auctions,
auctionDataExtended: state.auctionDataExtended,
metadataByMint: state.metadataByMint, metadataByMint: state.metadataByMint,
safetyDepositBoxesByVaultAndIndex: state.safetyDepositBoxesByVaultAndIndex, safetyDepositBoxesByVaultAndIndex:
bidderMetadataByAuctionAndBidder: state.bidderMetadataByAuctionAndBidder, state.safetyDepositBoxesByVaultAndIndex,
bidderMetadataByAuctionAndBidder:
state.bidderMetadataByAuctionAndBidder,
bidderPotsByAuctionAndBidder: state.bidderPotsByAuctionAndBidder, bidderPotsByAuctionAndBidder: state.bidderPotsByAuctionAndBidder,
vaults: state.vaults, vaults: state.vaults,
bidRedemptions: state.bidRedemptions, bidRedemptions: state.bidRedemptions,
@ -525,6 +558,21 @@ const processAuctions = (
// ignore errors // ignore errors
// add type as first byte for easier deserialization // add type as first byte for easier deserialization
} }
try {
if (a.account.data.length === MAX_AUCTION_DATA_EXTENDED_SIZE) {
const account = cache.add(
a.pubkey,
a.account,
AuctionDataExtendedParser,
false,
) as ParsedAccount<AuctionDataExtended>;
setter('auctionDataExtended', a.pubkey.toBase58(), account);
}
} catch {
// ignore errors
// add type as first byte for easier deserialization
}
try { try {
if (a.account.data.length === BIDDER_METADATA_LEN) { if (a.account.data.length === BIDDER_METADATA_LEN) {
const account = cache.add( const account = cache.add(
@ -536,9 +584,10 @@ const processAuctions = (
setter( setter(
'bidderMetadataByAuctionAndBidder', 'bidderMetadataByAuctionAndBidder',
account.info.auctionPubkey.toBase58() + account.info.auctionPubkey.toBase58() +
'-' + '-' +
account.info.bidderPubkey.toBase58(), account.info.bidderPubkey.toBase58(),
account); account,
);
} }
} catch { } catch {
// ignore errors // ignore errors
@ -555,9 +604,10 @@ const processAuctions = (
setter( setter(
'bidderPotsByAuctionAndBidder', 'bidderPotsByAuctionAndBidder',
account.info.auctionAct.toBase58() + account.info.auctionAct.toBase58() +
'-' + '-' +
account.info.bidderAct.toBase58(), account.info.bidderAct.toBase58(),
account); account,
);
} }
} catch { } catch {
// ignore errors // ignore errors
@ -591,7 +641,11 @@ const processMetaplexAccounts = async (
account: a.account, account: a.account,
info: auctionManager, info: auctionManager,
}; };
setter('auctionManagersByAuction', auctionManager.auction.toBase58(), account); setter(
'auctionManagersByAuction',
auctionManager.auction.toBase58(),
account,
);
} }
} }
} else if (a.account.data[0] === MetaplexKey.BidRedemptionTicketV1) { } else if (a.account.data[0] === MetaplexKey.BidRedemptionTicketV1) {
@ -645,7 +699,11 @@ const processMetaplexAccounts = async (
account.info.image = nameInfo.image; account.info.image = nameInfo.image;
account.info.twitter = nameInfo.twitter; account.info.twitter = nameInfo.twitter;
} }
setter('whitelistedCreatorsByCreator', whitelistedCreator.address.toBase58(), account); setter(
'whitelistedCreatorsByCreator',
whitelistedCreator.address.toBase58(),
account,
);
} }
} }
} catch { } catch {
@ -658,7 +716,8 @@ const processMetaData = (
meta: PublicKeyAndAccount<Buffer>, meta: PublicKeyAndAccount<Buffer>,
setter: UpdateStateValueFunc, setter: UpdateStateValueFunc,
) => { ) => {
if (meta.account.owner.toBase58() !== programIds().metadata.toBase58()) return; if (meta.account.owner.toBase58() !== programIds().metadata.toBase58())
return;
try { try {
if (meta.account.data[0] === MetadataKey.MetadataV1) { if (meta.account.data[0] === MetadataKey.MetadataV1) {
@ -692,8 +751,16 @@ const processMetaData = (
info: masterEdition, info: masterEdition,
}; };
setter('masterEditions', meta.pubkey.toBase58(), account); setter('masterEditions', meta.pubkey.toBase58(), account);
setter('masterEditionsByPrintingMint', masterEdition.printingMint.toBase58(), account); setter(
setter('masterEditionsByOneTimeAuthMint', masterEdition.oneTimePrintingAuthorizationMint.toBase58(), account); 'masterEditionsByPrintingMint',
masterEdition.printingMint.toBase58(),
account,
);
setter(
'masterEditionsByOneTimeAuthMint',
masterEdition.oneTimePrintingAuthorizationMint.toBase58(),
account,
);
} }
} catch { } catch {
// ignore errors // ignore errors
@ -717,7 +784,8 @@ const processVaultData = (
setter( setter(
'safetyDepositBoxesByVaultAndIndex', 'safetyDepositBoxesByVaultAndIndex',
safetyDeposit.vault.toBase58() + '-' + safetyDeposit.order, safetyDeposit.vault.toBase58() + '-' + safetyDeposit.order,
account); account,
);
} else if (a.account.data[0] === VaultKey.VaultV1) { } else if (a.account.data[0] === VaultKey.VaultV1) {
const vault = decodeVault(a.account.data); const vault = decodeVault(a.account.data);
const account: ParsedAccount<Vault> = { const account: ParsedAccount<Vault> = {
@ -726,10 +794,7 @@ const processVaultData = (
info: vault, info: vault,
}; };
setter( setter('vaults', a.pubkey.toBase58(), account);
'vaults',
a.pubkey.toBase58(),
account);
} }
} catch { } catch {
// ignore errors // ignore errors

View File

@ -30,6 +30,7 @@ import {
Creator, Creator,
PriceFloor, PriceFloor,
PriceFloorType, PriceFloorType,
IPartialCreateAuctionArgs,
} from '@oyster/common'; } from '@oyster/common';
import { import {
Connection, Connection,
@ -305,14 +306,29 @@ export const AuctionCreateView = () => {
console.log('Tiered settings', settings); console.log('Tiered settings', settings);
} }
const auctionSettings: IPartialCreateAuctionArgs = {
winners: winnerLimit,
endAuctionAt: new BN((attributes.auctionDuration || 0) * 60), // endAuctionAt is actually auction duration, poorly named, in seconds
auctionGap: new BN((attributes.gapTime || 0) * 60),
priceFloor: new PriceFloor({
type: attributes.priceFloor
? PriceFloorType.Minimum
: PriceFloorType.None,
minPrice: new BN((attributes.priceFloor || 0) * LAMPORTS_PER_SOL),
}),
tokenMint: QUOTE_MINT,
gapTickSizePercentage: attributes.tickSizeEndingPhase || null,
tickSize: attributes.priceTick
? new BN(attributes.priceTick * LAMPORTS_PER_SOL)
: null,
};
const _auctionObj = await createAuctionManager( const _auctionObj = await createAuctionManager(
connection, connection,
wallet, wallet,
whitelistedCreatorsByCreator, whitelistedCreatorsByCreator,
settings, settings,
winnerLimit, auctionSettings,
new BN((attributes.auctionDuration || 0) * 60), // endAuctionAt is actually auction duration, poorly named, in seconds
new BN((attributes.gapTime || 0) * 60),
attributes.category === AuctionCategory.Open attributes.category === AuctionCategory.Open
? [] ? []
: attributes.category !== AuctionCategory.Tiered : attributes.category !== AuctionCategory.Tiered
@ -322,12 +338,6 @@ export const AuctionCreateView = () => {
? attributes.items[0] ? attributes.items[0]
: attributes.participationNFT, : attributes.participationNFT,
QUOTE_MINT, QUOTE_MINT,
new PriceFloor({
type: attributes.priceFloor
? PriceFloorType.Minimum
: PriceFloorType.None,
minPrice: new BN((attributes.priceFloor || 0) * LAMPORTS_PER_SOL),
}),
); );
setAuctionObj(_auctionObj); setAuctionObj(_auctionObj);
}; };

1005
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -122,6 +122,18 @@ pub enum AuctionError {
/// Data type mismatch /// Data type mismatch
#[error("Data type mismatch")] #[error("Data type mismatch")]
DataTypeMismatch, DataTypeMismatch,
/// Bid must be multiple of tick size
#[error("Bid must be multiple of tick size")]
BidMustBeMultipleOfTickSize,
/// During the gap window, gap between next lowest bid must be of a certain percentage
#[error("During the gap window, gap between next lowest bid must be of a certain percentage")]
GapBetweenBidsTooSmall,
/// Gap tick size percentage must be between 0 and 100
#[error("Gap tick size percentage must be between 0 and 100")]
InvalidGapTickSizePercentage,
} }
impl PrintProgramError for AuctionError { impl PrintProgramError for AuctionError {

View File

@ -134,10 +134,10 @@ impl AuctionData {
(Some(end), Some(gap)) => { (Some(end), Some(gap)) => {
// Check if the bid is within the gap between the last bidder. // Check if the bid is within the gap between the last bidder.
if let Some(last) = self.last_bid { if let Some(last) = self.last_bid {
let next_bid_time = match last.checked_add(gap) { let next_bid_time = last
Some(val) => val, .checked_add(gap)
None => return Err(AuctionError::NumericalOverflowError.into()), .ok_or(AuctionError::NumericalOverflowError)?;
};
Ok(now > end && now > next_bid_time) Ok(now > end && now > next_bid_time)
} else { } else {
Ok(now > end) Ok(now > end)
@ -179,6 +179,28 @@ impl AuctionData {
}; };
self.bid_state.winner_at(idx, minimum) self.bid_state.winner_at(idx, minimum)
} }
pub fn place_bid(
&mut self,
bid: Bid,
tick_size: Option<u64>,
gap_tick_size_percentage: Option<u8>,
now: UnixTimestamp,
) -> Result<(), ProgramError> {
let gap_val = match self.ended_at {
Some(end) => {
// We use the actual gap tick size perc if we're in gap window,
// otherwise we pass in none so the logic isnt used
if now > end {
gap_tick_size_percentage
} else {
None
}
}
None => None,
};
self.bid_state.place_bid(bid, tick_size, gap_val)
}
} }
/// Define valid auction state transitions. /// Define valid auction state transitions.
@ -259,59 +281,120 @@ impl BidState {
real_max real_max
} }
fn assert_valid_tick_size_bid(bid: &Bid, tick_size: Option<u64>) -> ProgramResult {
if let Some(tick) = tick_size {
if bid.1.checked_rem(tick) != Some(0) {
msg!(
"This bid {:?} is not a multiple of tick size {:?}, throw it out.",
bid.1,
tick_size
);
return Err(AuctionError::BidMustBeMultipleOfTickSize.into());
}
} else {
msg!("No tick size on this auction")
}
Ok(())
}
fn assert_valid_gap_insertion(
gap_tick: u8,
beaten_bid: &Bid,
beating_bid: &Bid,
) -> ProgramResult {
// Use u128 to avoid potential overflow due to temporary mult of 100x since
// we haven't divided yet.
let mut minimum_bid_amount: u128 = (beaten_bid.1 as u128)
.checked_mul((100 + gap_tick) as u128)
.ok_or(AuctionError::NumericalOverflowError)?;
minimum_bid_amount = minimum_bid_amount
.checked_div(100u128)
.ok_or(AuctionError::NumericalOverflowError)?;
if minimum_bid_amount > beating_bid.1 as u128 {
msg!("Rejecting inserting this bid due to gap tick size of {:?} which causes min bid of {:?} from {:?} which is the bid it is trying to beat", gap_tick, minimum_bid_amount.to_string(), beaten_bid.1);
return Err(AuctionError::GapBetweenBidsTooSmall.into());
}
Ok(())
}
/// Push a new bid into the state, this succeeds only if the bid is larger than the current top /// Push a new bid into the state, this succeeds only if the bid is larger than the current top
/// winner stored. Crappy list information to start with. /// winner stored. Crappy list information to start with.
pub fn place_bid(&mut self, bid: Bid) -> Result<(), ProgramError> { pub fn place_bid(
&mut self,
bid: Bid,
tick_size: Option<u64>,
gap_tick_size_percentage: Option<u8>,
) -> Result<(), ProgramError> {
msg!("Placing bid {:?}", &bid.1.to_string());
BidState::assert_valid_tick_size_bid(&bid, tick_size)?;
match self { match self {
// In a capped auction, track the limited number of winners. // In a capped auction, track the limited number of winners.
BidState::EnglishAuction { ref mut bids, max } => match bids.last() { BidState::EnglishAuction { ref mut bids, max } => {
Some(top) => { match bids.last() {
msg!("Looking to go over the loop"); Some(top) => {
for i in (0..bids.len()).rev() { msg!("Looking to go over the loop, but check tick size first");
msg!("Comparison of {:?} and {:?} for {:?}", bids[i].1, bid.1, i);
if bids[i].1 < bid.1 { for i in (0..bids.len()).rev() {
msg!("Ok we can do an insert"); msg!("Comparison of {:?} and {:?} for {:?}", bids[i].1, bid.1, i);
if i + 1 < bids.len() { if bids[i].1 < bid.1 {
msg!("Doing a normal insert"); if let Some(gap_tick) = gap_tick_size_percentage {
bids.insert(i + 1, bid); BidState::assert_valid_gap_insertion(gap_tick, &bids[i], &bid)?
} else { }
msg!("Doing an on the end insert");
bids.push(bid) msg!("Ok we can do an insert");
} if i + 1 < bids.len() {
break; msg!("Doing a normal insert");
} else if bids[i].1 == bid.1 { bids.insert(i + 1, bid);
msg!("Ok we can do an equivalent insert"); } else {
if i == 0 { msg!("Doing an on the end insert");
msg!("Doing a normal insert"); bids.push(bid)
}
break;
} else if bids[i].1 == bid.1 {
if let Some(gap_tick) = gap_tick_size_percentage {
if gap_tick > 0 {
msg!("Rejecting same-bid insert due to gap tick size of {:?}", gap_tick);
return Err(AuctionError::GapBetweenBidsTooSmall.into());
}
}
msg!("Ok we can do an equivalent insert");
if i == 0 {
msg!("Doing a normal insert");
bids.insert(0, bid);
break;
} else {
if bids[i - 1].1 != bids[i].1 {
msg!("Doing an insert just before");
bids.insert(i, bid);
break;
}
msg!("More duplicates ahead...")
}
} else if i == 0 {
msg!("Inserting at 0");
bids.insert(0, bid); bids.insert(0, bid);
break; break;
} else {
if bids[i - 1].1 != bids[i].1 {
msg!("Doing an insert just before");
bids.insert(i, bid);
break;
}
msg!("More duplicates ahead...")
} }
} else if i == 0 {
msg!("Inserting at 0");
bids.insert(0, bid);
break;
} }
} let max_size = BidState::max_array_size_for(*max);
let max_size = BidState::max_array_size_for(*max);
if bids.len() > max_size { if bids.len() > max_size {
bids.remove(0); bids.remove(0);
}
Ok(())
}
_ => {
msg!("Pushing bid onto stack");
bids.push(bid);
Ok(())
} }
Ok(())
} }
_ => { }
msg!("Pushing bid onto stack");
bids.push(bid);
Ok(())
}
},
// In an open auction, bidding simply succeeds. // In an open auction, bidding simply succeeds.
BidState::OpenEdition { bids, max } => Ok(()), BidState::OpenEdition { bids, max } => Ok(()),

View File

@ -40,6 +40,10 @@ pub struct CreateAuctionArgs {
pub resource: Pubkey, pub resource: Pubkey,
/// Set a price floor. /// Set a price floor.
pub price_floor: PriceFloor, pub price_floor: PriceFloor,
/// Add a tick size increment
pub tick_size: Option<u64>,
/// Add a minimum percentage increase each bid must meet.
pub gap_tick_size_percentage: Option<u8>,
} }
struct Accounts<'a, 'b: 'a> { struct Accounts<'a, 'b: 'a> {
@ -98,6 +102,12 @@ pub fn create_auction(
WinnerLimit::Unlimited(_) => BidState::new_open_edition(), WinnerLimit::Unlimited(_) => BidState::new_open_edition(),
}; };
if let Some(gap_tick) = args.gap_tick_size_percentage {
if gap_tick > 100 {
return Err(AuctionError::InvalidGapTickSizePercentage.into());
}
}
// Create auction account with enough space for a winner tracking. // Create auction account with enough space for a winner tracking.
create_or_allocate_account_raw( create_or_allocate_account_raw(
*program_id, *program_id,
@ -141,6 +151,14 @@ pub fn create_auction(
], ],
)?; )?;
// Configure extended
AuctionDataExtended {
total_uncancelled_bids: 0,
tick_size: args.tick_size,
gap_tick_size_percentage: args.gap_tick_size_percentage,
}
.serialize(&mut *accounts.auction_extended.data.borrow_mut())?;
// Configure Auction. // Configure Auction.
AuctionData { AuctionData {
authority: args.authority, authority: args.authority,

View File

@ -316,9 +316,12 @@ pub fn place_bid<'r, 'b: 'r>(
// Serialize new Auction State // Serialize new Auction State
auction.last_bid = Some(clock.unix_timestamp); auction.last_bid = Some(clock.unix_timestamp);
auction auction.place_bid(
.bid_state Bid(*accounts.bidder.key, args.amount),
.place_bid(Bid(*accounts.bidder.key, args.amount))?; auction_extended.tick_size,
auction_extended.gap_tick_size_percentage,
clock.unix_timestamp,
)?;
auction.serialize(&mut *accounts.auction.data.borrow_mut())?; auction.serialize(&mut *accounts.auction.data.borrow_mut())?;
// Update latest metadata with results from the bid. // Update latest metadata with results from the bid.

View File

@ -21,12 +21,60 @@ use {
}, },
}; };
fn set_reservation_list_wrapper<'a>(
program_id: &'a Pubkey,
master_edition_info: &AccountInfo<'a>,
reservation_list_info: &AccountInfo<'a>,
auction_manager_info: &AccountInfo<'a>,
signer_seeds: &[&[u8]],
reservations: Vec<Reservation>,
total_reservation_spots: Option<u64>,
offset: u64,
total_spot_offset: u64,
) -> ProgramResult {
invoke_signed(
&set_reservation_list(
*program_id,
*master_edition_info.key,
*reservation_list_info.key,
*auction_manager_info.key,
reservations,
total_reservation_spots,
offset,
total_spot_offset,
),
&[
master_edition_info.clone(),
reservation_list_info.clone(),
auction_manager_info.clone(),
],
&[&signer_seeds],
)?;
Ok(())
}
pub fn calc_spots(
winning_config_item: &WinningConfigItem,
auction_manager: &AuctionManager,
n: usize,
) -> u64 {
auction_manager.settings.winning_configs[n]
.items
.iter()
.filter(|i| i.safety_deposit_box_index == winning_config_item.safety_deposit_box_index)
.map(|i| i.amount as u64)
.sum()
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn reserve_list_if_needed<'a>( pub fn reserve_list_if_needed<'a>(
program_id: &'a Pubkey, program_id: &'a Pubkey,
auction_manager: &AuctionManager, auction_manager: &AuctionManager,
auction: &AuctionData, auction: &AuctionData,
winning_config_item: &WinningConfigItem, winning_config_item: &WinningConfigItem,
winning_index: usize,
bidder_info: &AccountInfo<'a>,
master_edition_info: &AccountInfo<'a>, master_edition_info: &AccountInfo<'a>,
reservation_list_info: &AccountInfo<'a>, reservation_list_info: &AccountInfo<'a>,
auction_manager_info: &AccountInfo<'a>, auction_manager_info: &AccountInfo<'a>,
@ -34,57 +82,56 @@ pub fn reserve_list_if_needed<'a>(
) -> ProgramResult { ) -> ProgramResult {
let reservation_list = get_reservation_list(reservation_list_info)?; let reservation_list = get_reservation_list(reservation_list_info)?;
if reservation_list.supply_snapshot().is_none() { let total_reservation_spot_opt: Option<u64>;
let mut reservations: Vec<Reservation> = vec![];
// Auction specifically does not expose internal state workings as it may change someday, // Auction specifically does not expose internal state workings as it may change someday,
// but it does expose a point get-winner-at-index method. Right now this is just array access // but it does expose a point get-winner-at-index method. Right now this is just array access
// but may be invocation someday. It's inefficient style but better for the interface maintenance // but may be invocation someday. It's inefficient style but better for the interface maintenance
// in the long run if we move to better storage solutions (so that this action doesnt need to change if // in the long run if we move to better storage solutions (so that this action doesnt need to change if
// storage does.) // storage does.)
for n in 0..auction_manager.settings.winning_configs.len() { let mut total_reservation_spots: u64 = 0;
match auction.winner_at(n) { let mut total_spot_offset: u64 = 0;
Some(address) => { for n in 0..auction_manager.settings.winning_configs.len() {
let spots: u64 = auction_manager.settings.winning_configs[n] match auction.winner_at(n) {
.items Some(_) => {
.iter() let spots: u64 = calc_spots(winning_config_item, auction_manager, n);
.filter(|i| { total_reservation_spots = total_reservation_spots
i.safety_deposit_box_index .checked_add(spots)
== winning_config_item.safety_deposit_box_index .ok_or(MetaplexError::NumericalOverflowError)?;
}) if n < winning_index {
.map(|i| i.amount as u64) total_spot_offset = total_spot_offset
.sum(); .checked_add(spots)
reservations.push(Reservation { .ok_or(MetaplexError::NumericalOverflowError)?;
address,
// Select all items in a winning config matching the same safety deposit box
// as the one being redeemed here (likely only one)
// and then sum them to get the total spots to reserve for this winner
spots_remaining: spots,
total_spots: spots,
})
} }
None => break,
} }
None => break,
} }
invoke_signed(
&set_reservation_list(
*program_id,
*master_edition_info.key,
*reservation_list_info.key,
*auction_manager_info.key,
reservations,
),
&[
master_edition_info.clone(),
reservation_list_info.clone(),
auction_manager_info.clone(),
],
&[&signer_seeds],
)?;
} }
if reservation_list.supply_snapshot().is_none() {
total_reservation_spot_opt = Some(total_reservation_spots)
} else {
total_reservation_spot_opt = None
}
let my_spots: u64 = calc_spots(winning_config_item, auction_manager, winning_index);
set_reservation_list_wrapper(
program_id,
master_edition_info,
reservation_list_info,
auction_manager_info,
signer_seeds,
vec![Reservation {
address: *bidder_info.key,
spots_remaining: my_spots,
total_spots: my_spots,
}],
total_reservation_spot_opt,
winning_index as u64,
total_spot_offset,
)?;
Ok(()) Ok(())
} }
pub fn process_redeem_bid<'a>( pub fn process_redeem_bid<'a>(
@ -186,6 +233,8 @@ pub fn process_redeem_bid<'a>(
&auction_manager, &auction_manager,
&auction, &auction,
&winning_config_item, &winning_config_item,
winning_index,
bidder_info,
master_edition_info, master_edition_info,
reservation_list_info, reservation_list_info,
auction_manager_info, auction_manager_info,

View File

@ -266,6 +266,18 @@ pub enum MetadataError {
/// Data type mismatch /// Data type mismatch
#[error("Data type mismatch")] #[error("Data type mismatch")]
DataTypeMismatch, DataTypeMismatch,
/// Beyond alotted address size in reservation!
#[error("Beyond alotted address size in reservation!")]
BeyondAlottedAddressSize,
/// The reservation has only been partially alotted
#[error("The reservation has only been partially alotted")]
ReservationNotComplete,
/// You cannot splice over an existing reservation!
#[error("You cannot splice over an existing reservation!")]
TriedToReplaceAnExistingReservation,
} }
impl PrintProgramError for MetadataError { impl PrintProgramError for MetadataError {

View File

@ -45,6 +45,15 @@ pub struct MintPrintingTokensViaTokenArgs {
pub struct SetReservationListArgs { pub struct SetReservationListArgs {
/// If set, means that no more than this number of editions can ever be minted. This is immutable. /// If set, means that no more than this number of editions can ever be minted. This is immutable.
pub reservations: Vec<Reservation>, pub reservations: Vec<Reservation>,
/// should only be present on the very first call to set reservation list.
pub total_reservation_spots: Option<u64>,
/// Where in the reservation list you want to insert this slice of reservations
pub offset: u64,
/// What the total spot offset is in the reservation list from the beginning to your slice of reservations.
/// So if is going to be 4 total editions eventually reserved between your slice and the beginning of the array,
/// split between 2 reservation entries, the offset variable above would be "2" since you start at entry 2 in 0 indexed array
/// (first 2 taking 0 and 1) and because they each have 2 spots taken, this variable would be 4.
pub total_spot_offset: u64,
} }
/// Instructions supported by the Metadata program. /// Instructions supported by the Metadata program.
@ -124,9 +133,13 @@ pub enum MetadataInstruction {
/// with the pda that was created by that first bidder - the token metadata can then cross reference /// with the pda that was created by that first bidder - the token metadata can then cross reference
/// these people with the list and see that bidder A gets edition #2, so on and so forth. /// these people with the list and see that bidder A gets edition #2, so on and so forth.
/// ///
/// NOTE: If you have more than 20 addresses in a reservation list, this may be called multiple times to build up the list,
/// otherwise, it simply wont fit in one transaction. Only provide a total_reservation argument on the first call, which will
/// allocate the edition space, and in follow up calls this will specifically be unnecessary (and indeed will error.)
///
/// 0. `[writable]` Master Edition key (pda of ['metadata', program id, mint id, 'edition']) /// 0. `[writable]` Master Edition key (pda of ['metadata', program id, mint id, 'edition'])
/// 1. `[writable]` PDA for ReservationList of ['metadata', program id, master edition key, 'reservation', resource-key] /// 1. `[writable]` PDA for ReservationList of ['metadata', program id, master edition key, 'reservation', resource-key]
/// 3. `[signer]` The resource you tied the reservation list too /// 2. `[signer]` The resource you tied the reservation list too
SetReservationList(SetReservationListArgs), SetReservationList(SetReservationListArgs),
/// Create an empty reservation list for a resource who can come back later as a signer and fill the reservation list /// Create an empty reservation list for a resource who can come back later as a signer and fill the reservation list
@ -363,6 +376,9 @@ pub fn set_reservation_list(
reservation_list: Pubkey, reservation_list: Pubkey,
resource: Pubkey, resource: Pubkey,
reservations: Vec<Reservation>, reservations: Vec<Reservation>,
total_reservation_spots: Option<u64>,
offset: u64,
total_spot_offset: u64,
) -> Instruction { ) -> Instruction {
Instruction { Instruction {
program_id, program_id,
@ -371,9 +387,14 @@ pub fn set_reservation_list(
AccountMeta::new(reservation_list, false), AccountMeta::new(reservation_list, false),
AccountMeta::new_readonly(resource, true), AccountMeta::new_readonly(resource, true),
], ],
data: MetadataInstruction::SetReservationList(SetReservationListArgs { reservations }) data: MetadataInstruction::SetReservationList(SetReservationListArgs {
.try_to_vec() reservations,
.unwrap(), total_reservation_spots,
offset,
total_spot_offset,
})
.try_to_vec()
.unwrap(),
} }
} }

View File

@ -70,7 +70,14 @@ pub fn process_instruction(
} }
MetadataInstruction::SetReservationList(args) => { MetadataInstruction::SetReservationList(args) => {
msg!("Instruction: Set Reservation List"); msg!("Instruction: Set Reservation List");
process_set_reservation_list(program_id, accounts, args.reservations) process_set_reservation_list(
program_id,
accounts,
args.reservations,
args.total_reservation_spots,
args.offset,
args.total_spot_offset,
)
} }
MetadataInstruction::CreateReservationList => { MetadataInstruction::CreateReservationList => {
msg!("Instruction: Create Reservation List"); msg!("Instruction: Create Reservation List");
@ -541,6 +548,9 @@ pub fn process_set_reservation_list(
program_id: &Pubkey, program_id: &Pubkey,
accounts: &[AccountInfo], accounts: &[AccountInfo],
reservations: Vec<Reservation>, reservations: Vec<Reservation>,
total_reservation_spots: Option<u64>,
offset: u64,
total_spot_offset: u64,
) -> ProgramResult { ) -> ProgramResult {
let account_info_iter = &mut accounts.iter(); let account_info_iter = &mut accounts.iter();
@ -558,10 +568,6 @@ pub fn process_set_reservation_list(
return Err(MetadataError::ReservationDoesNotExist.into()); return Err(MetadataError::ReservationDoesNotExist.into());
} }
if reservations.len() > MAX_RESERVATIONS {
return Err(MetadataError::BeyondMaxAddressSize.into());
}
assert_derivation( assert_derivation(
program_id, program_id,
reservation_list_info, reservation_list_info,
@ -576,12 +582,12 @@ pub fn process_set_reservation_list(
let mut reservation_list = get_reservation_list(reservation_list_info)?; let mut reservation_list = get_reservation_list(reservation_list_info)?;
if reservation_list.supply_snapshot().is_some() { if reservation_list.supply_snapshot().is_some() && total_reservation_spots.is_some() {
return Err(MetadataError::ReservationAlreadyMade.into()); return Err(MetadataError::ReservationAlreadyMade.into());
} }
let mut total_len: u64 = 0; let mut total_len: u64 = reservation_list.current_reservation_spots();
let mut total_len_check: u64 = 0; let mut total_len_check: u64 = reservation_list.current_reservation_spots();
for reservation in &reservations { for reservation in &reservations {
total_len = total_len total_len = total_len
@ -596,28 +602,39 @@ pub fn process_set_reservation_list(
); );
} }
} }
reservation_list.set_current_reservation_spots(total_len);
reservation_list.add_reservations(reservations, offset, total_spot_offset)?;
if let Some(total) = total_reservation_spots {
reservation_list.set_supply_snapshot(Some(master_edition.supply));
reservation_list.set_total_reservation_spots(total);
master_edition.supply = master_edition
.supply
.checked_add(total as u64)
.ok_or(MetadataError::NumericalOverflowError)?;
if let Some(max_supply) = master_edition.max_supply {
if master_edition.supply > max_supply {
return Err(MetadataError::ReservationBreachesMaximumSupply.into());
}
}
master_edition.serialize(&mut *master_edition_info.data.borrow_mut())?;
}
if total_len_check != total_len { if total_len_check != total_len {
return Err(MetadataError::SpotMismatch.into()); return Err(MetadataError::SpotMismatch.into());
} }
reservation_list.set_supply_snapshot(Some(master_edition.supply)); if total_len > reservation_list.total_reservation_spots() {
reservation_list.set_reservations(reservations); return Err(MetadataError::BeyondAlottedAddressSize.into());
msg!("Master edition {:?}", master_edition); };
msg!("Total new spots {:?}", total_len);
master_edition.supply = master_edition
.supply
.checked_add(total_len as u64)
.ok_or(MetadataError::NumericalOverflowError)?;
if let Some(max_supply) = master_edition.max_supply { if reservation_list.reservations().len() > MAX_RESERVATIONS {
if master_edition.supply > max_supply { return Err(MetadataError::BeyondMaxAddressSize.into());
return Err(MetadataError::ReservationBreachesMaximumSupply.into());
}
} }
reservation_list.save(reservation_list_info)?; reservation_list.save(reservation_list_info)?;
master_edition.serialize(&mut *master_edition_info.data.borrow_mut())?;
Ok(()) Ok(())
} }

View File

@ -46,7 +46,7 @@ pub const MAX_RESERVATIONS: usize = 200;
pub const MAX_RESERVATION_LIST_V1_SIZE: usize = 1 + 32 + 8 + 8 + MAX_RESERVATIONS * 34 + 100; pub const MAX_RESERVATION_LIST_V1_SIZE: usize = 1 + 32 + 8 + 8 + MAX_RESERVATIONS * 34 + 100;
// can hold up to 200 keys per reservation, note: the extra 8 is for number of elements in the vec // can hold up to 200 keys per reservation, note: the extra 8 is for number of elements in the vec
pub const MAX_RESERVATION_LIST_SIZE: usize = 1 + 32 + 8 + 8 + MAX_RESERVATIONS * 48 + 100; pub const MAX_RESERVATION_LIST_SIZE: usize = 1 + 32 + 8 + 8 + MAX_RESERVATIONS * 48 + 8 + 8 + 84;
#[repr(C)] #[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
@ -170,9 +170,19 @@ pub trait ReservationList {
fn master_edition(&self) -> Pubkey; fn master_edition(&self) -> Pubkey;
fn supply_snapshot(&self) -> Option<u64>; fn supply_snapshot(&self) -> Option<u64>;
fn reservations(&self) -> Vec<Reservation>; fn reservations(&self) -> Vec<Reservation>;
fn total_reservation_spots(&self) -> u64;
fn current_reservation_spots(&self) -> u64;
fn set_master_edition(&mut self, key: Pubkey); fn set_master_edition(&mut self, key: Pubkey);
fn set_supply_snapshot(&mut self, supply: Option<u64>); fn set_supply_snapshot(&mut self, supply: Option<u64>);
fn set_reservations(&mut self, reservations: Vec<Reservation>); fn set_reservations(&mut self, reservations: Vec<Reservation>) -> ProgramResult;
fn add_reservations(
&mut self,
reservations: Vec<Reservation>,
offset: u64,
total_spot_offset: u64,
) -> ProgramResult;
fn set_total_reservation_spots(&mut self, total_reservation_spots: u64);
fn set_current_reservation_spots(&mut self, current_reservation_spots: u64);
fn save(&self, account: &AccountInfo) -> ProgramResult; fn save(&self, account: &AccountInfo) -> ProgramResult;
} }
@ -199,6 +209,10 @@ pub struct ReservationListV2 {
/// What supply counter was on master_edition when this reservation was created. /// What supply counter was on master_edition when this reservation was created.
pub supply_snapshot: Option<u64>, pub supply_snapshot: Option<u64>,
pub reservations: Vec<Reservation>, pub reservations: Vec<Reservation>,
/// How many reservations there are going to be, given on first set_reservation call
pub total_reservation_spots: u64,
/// Cached count of reservation spots in the reservation vec to save on CPU.
pub current_reservation_spots: u64,
} }
impl ReservationList for ReservationListV2 { impl ReservationList for ReservationListV2 {
@ -222,14 +236,74 @@ impl ReservationList for ReservationListV2 {
self.supply_snapshot = supply; self.supply_snapshot = supply;
} }
fn set_reservations(&mut self, reservations: Vec<Reservation>) { fn add_reservations(
self.reservations = reservations &mut self,
mut reservations: Vec<Reservation>,
offset: u64,
total_spot_offset: u64,
) -> ProgramResult {
let usize_offset = offset as usize;
while self.reservations.len() < usize_offset {
self.reservations.push(Reservation {
address: solana_program::system_program::id(),
spots_remaining: 0,
total_spots: 0,
})
}
if self.reservations.len() > usize_offset {
let removed_elements: Vec<Reservation> = self
.reservations
.splice(
usize_offset..usize_offset + reservations.len(),
reservations,
)
.collect();
let existing_res = removed_elements
.iter()
.find(|r| r.address != solana_program::system_program::id());
if existing_res.is_some() {
return Err(MetadataError::TriedToReplaceAnExistingReservation.into());
}
} else {
self.reservations.append(&mut reservations)
}
if usize_offset != 0
&& self.reservations[usize_offset - 1].address == solana_program::system_program::id()
{
// This becomes an anchor then for calculations... put total spot offset in here.
self.reservations[usize_offset - 1].spots_remaining = total_spot_offset;
self.reservations[usize_offset - 1].total_spots = total_spot_offset;
}
Ok(())
}
fn set_reservations(&mut self, reservations: Vec<Reservation>) -> ProgramResult {
self.reservations = reservations;
Ok(())
} }
fn save(&self, account: &AccountInfo) -> ProgramResult { fn save(&self, account: &AccountInfo) -> ProgramResult {
self.serialize(&mut *account.data.borrow_mut())?; self.serialize(&mut *account.data.borrow_mut())?;
Ok(()) Ok(())
} }
fn total_reservation_spots(&self) -> u64 {
self.total_reservation_spots
}
fn set_total_reservation_spots(&mut self, total_reservation_spots: u64) {
self.total_reservation_spots = total_reservation_spots;
}
fn current_reservation_spots(&self) -> u64 {
self.current_reservation_spots
}
fn set_current_reservation_spots(&mut self, current_reservation_spots: u64) {
self.current_reservation_spots = current_reservation_spots;
}
} }
impl ReservationListV2 { impl ReservationListV2 {
@ -293,7 +367,12 @@ impl ReservationList for ReservationListV1 {
self.supply_snapshot = supply; self.supply_snapshot = supply;
} }
fn set_reservations(&mut self, reservations: Vec<Reservation>) { fn add_reservations(
&mut self,
reservations: Vec<Reservation>,
_: u64,
_: u64,
) -> ProgramResult {
self.reservations = reservations self.reservations = reservations
.iter() .iter()
.map(|r| ReservationV1 { .map(|r| ReservationV1 {
@ -302,12 +381,31 @@ impl ReservationList for ReservationListV1 {
total_spots: r.total_spots as u8, total_spots: r.total_spots as u8,
}) })
.collect(); .collect();
Ok(())
}
fn set_reservations(&mut self, reservations: Vec<Reservation>) -> ProgramResult {
self.add_reservations(reservations, 0, 0)?;
Ok(())
} }
fn save(&self, account: &AccountInfo) -> ProgramResult { fn save(&self, account: &AccountInfo) -> ProgramResult {
self.serialize(&mut *account.data.borrow_mut())?; self.serialize(&mut *account.data.borrow_mut())?;
Ok(()) Ok(())
} }
fn total_reservation_spots(&self) -> u64 {
self.reservations.iter().map(|r| r.total_spots as u64).sum()
}
fn set_total_reservation_spots(&mut self, _: u64) {}
fn current_reservation_spots(&self) -> u64 {
self.reservations.iter().map(|r| r.total_spots as u64).sum()
}
fn set_current_reservation_spots(&mut self, _: u64) {}
} }
impl ReservationListV1 { impl ReservationListV1 {

View File

@ -424,6 +424,7 @@ pub fn mint_limited_edition<'a>(
let mut reservations = reservation_list.reservations(); let mut reservations = reservation_list.reservations();
for i in 0..reservations.len() { for i in 0..reservations.len() {
let mut reservation = &mut reservations[i]; let mut reservation = &mut reservations[i];
if reservation.address == *mint_authority_info.key { if reservation.address == *mint_authority_info.key {
offset = Some( offset = Some(
prev_total_offsets prev_total_offsets
@ -436,13 +437,21 @@ pub fn mint_limited_edition<'a>(
.checked_sub(1) .checked_sub(1)
.ok_or(MetadataError::NumericalOverflowError)?; .ok_or(MetadataError::NumericalOverflowError)?;
reservation_list.set_reservations(reservations); reservation_list.set_reservations(reservations)?;
reservation_list.save(account)?; reservation_list.save(account)?;
break; break;
} }
prev_total_offsets = prev_total_offsets
.checked_add(reservation.total_spots) if reservation.address == solana_program::system_program::id() {
.ok_or(MetadataError::NumericalOverflowError)?; // This is an anchor point in the array...it means we reset our math to
// this offset because we may be missing information in between this point and
// the points before it.
prev_total_offsets = reservation.total_spots;
} else {
prev_total_offsets = prev_total_offsets
.checked_add(reservation.total_spots)
.ok_or(MetadataError::NumericalOverflowError)?;
}
} }
match offset { match offset {

View File

@ -5,7 +5,9 @@ use {
input_validators::{is_url, is_valid_pubkey, is_valid_signer}, input_validators::{is_url, is_valid_pubkey, is_valid_signer},
}, },
solana_client::rpc_client::RpcClient, solana_client::rpc_client::RpcClient,
solana_program::{borsh::try_from_slice_unchecked, program_pack::Pack}, solana_program::{
account_info::AccountInfo, borsh::try_from_slice_unchecked, program_pack::Pack,
},
solana_sdk::{ solana_sdk::{
pubkey::Pubkey, pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signer}, signature::{read_keypair_file, Keypair, Signer},
@ -22,7 +24,9 @@ use {
mint_new_edition_from_master_edition_via_token, mint_printing_tokens, mint_new_edition_from_master_edition_via_token, mint_printing_tokens,
update_metadata_accounts, update_metadata_accounts,
}, },
state::{Data, Edition, Key, MasterEdition, Metadata, EDITION, PREFIX}, state::{
get_reservation_list, Data, Edition, Key, MasterEdition, Metadata, EDITION, PREFIX,
},
}, },
std::str::FromStr, std::str::FromStr,
}; };
@ -79,6 +83,30 @@ fn mint_coins(app_matches: &ArgMatches, payer: Keypair, client: RpcClient) {
println!("Minted {:?} tokens to {:?}.", amount, destination_key); println!("Minted {:?} tokens to {:?}.", amount, destination_key);
} }
fn show_reservation_list(app_matches: &ArgMatches, _payer: Keypair, client: RpcClient) {
let key = pubkey_of(app_matches, "key").unwrap();
let mut res_data = client.get_account(&key).unwrap();
let mut lamports = 0;
let account_info = AccountInfo::new(
&key,
false,
false,
&mut lamports,
&mut res_data.data,
&res_data.owner,
false,
0,
);
let res_list = get_reservation_list(&account_info).unwrap();
println!("Res list {:?}", res_list.reservations());
println!(
"current res spots: {:?}",
res_list.current_reservation_spots()
);
println!("total res spots: {:?}", res_list.total_reservation_spots());
println!("supply snapshot: {:?}", res_list.supply_snapshot());
}
fn show(app_matches: &ArgMatches, _payer: Keypair, client: RpcClient) { fn show(app_matches: &ArgMatches, _payer: Keypair, client: RpcClient) {
let program_key = spl_token_metadata::id(); let program_key = spl_token_metadata::id();
@ -735,6 +763,18 @@ fn main() {
.takes_value(true) .takes_value(true)
.help("Metadata mint"), .help("Metadata mint"),
) )
).subcommand(
SubCommand::with_name("show_reservation_list")
.about("Show Reservation List")
.arg(
Arg::with_name("key")
.long("key")
.value_name("KEY")
.required(true)
.validator(is_valid_pubkey)
.takes_value(true)
.help("Account key of reservation list"),
)
) )
.subcommand( .subcommand(
SubCommand::with_name("create_master_edition") SubCommand::with_name("create_master_edition")
@ -843,6 +883,9 @@ fn main() {
("show", Some(arg_matches)) => { ("show", Some(arg_matches)) => {
show(arg_matches, payer, client); show(arg_matches, payer, client);
} }
("show_reservation_list", Some(arg_matches)) => {
show_reservation_list(arg_matches, payer, client);
}
("mint_coins", Some(arg_matches)) => { ("mint_coins", Some(arg_matches)) => {
mint_coins(arg_matches, payer, client); mint_coins(arg_matches, payer, client);
} }