Tick Size and Gap Tick Size (#66)
* Draft code for tick sizes * Hooking up front to back end, need to test it now. * Don't adjust ids. * Fully working tick size and gap tick percentage * Final fixes and validations for tick and gap * Update cargo.lock
This commit is contained in:
parent
7bfbf0f0af
commit
3ec50ee190
|
@ -15,6 +15,7 @@ import { findProgramAddress } from '../utils';
|
|||
export const AUCTION_PREFIX = 'auction';
|
||||
export const METADATA = 'metadata';
|
||||
export const EXTENDED = 'extended';
|
||||
export const MAX_AUCTION_DATA_EXTENDED_SIZE = 8 + 9 + 2 + 200;
|
||||
|
||||
export enum AuctionState {
|
||||
Created = 0,
|
||||
|
@ -91,6 +92,23 @@ export const decodeBidderPot = (buffer: Buffer) => {
|
|||
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 = (
|
||||
pubkey: PublicKey,
|
||||
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;
|
||||
/// How many winners are allowed for this auction. See AuctionData.
|
||||
winners: WinnerLimit;
|
||||
|
@ -340,6 +375,10 @@ class CreateAuctionArgs {
|
|||
|
||||
priceFloor: PriceFloor;
|
||||
|
||||
tickSize: BN | null;
|
||||
|
||||
gapTickSizePercentage: number | null;
|
||||
|
||||
constructor(args: {
|
||||
winners: WinnerLimit;
|
||||
endAuctionAt: BN | null;
|
||||
|
@ -348,6 +387,8 @@ class CreateAuctionArgs {
|
|||
authority: PublicKey;
|
||||
resource: PublicKey;
|
||||
priceFloor: PriceFloor;
|
||||
tickSize: BN | null;
|
||||
gapTickSizePercentage: number | null;
|
||||
}) {
|
||||
this.winners = args.winners;
|
||||
this.endAuctionAt = args.endAuctionAt;
|
||||
|
@ -356,6 +397,8 @@ class CreateAuctionArgs {
|
|||
this.authority = args.authority;
|
||||
this.resource = args.resource;
|
||||
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'],
|
||||
['resource', 'pubkey'],
|
||||
['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(
|
||||
winners: WinnerLimit,
|
||||
resource: PublicKey,
|
||||
endAuctionAt: BN | null,
|
||||
auctionGap: BN | null,
|
||||
priceFloor: PriceFloor,
|
||||
tokenMint: PublicKey,
|
||||
authority: PublicKey,
|
||||
settings: CreateAuctionArgs,
|
||||
creator: PublicKey,
|
||||
instructions: TransactionInstruction[],
|
||||
) {
|
||||
const auctionProgramId = programIds().auction;
|
||||
|
||||
const data = Buffer.from(
|
||||
serialize(
|
||||
AUCTION_SCHEMA,
|
||||
new CreateAuctionArgs({
|
||||
winners,
|
||||
resource,
|
||||
endAuctionAt,
|
||||
auctionGap,
|
||||
tokenMint,
|
||||
authority,
|
||||
priceFloor,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const data = Buffer.from(serialize(AUCTION_SCHEMA, settings));
|
||||
|
||||
const auctionKey: PublicKey = (
|
||||
await findProgramAddress(
|
||||
[
|
||||
Buffer.from(AUCTION_PREFIX),
|
||||
auctionProgramId.toBuffer(),
|
||||
resource.toBuffer(),
|
||||
settings.resource.toBuffer(),
|
||||
],
|
||||
auctionProgramId,
|
||||
)
|
||||
|
@ -591,7 +617,10 @@ export async function createAuction(
|
|||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: await getAuctionExtended({ auctionProgramId, resource }),
|
||||
pubkey: await getAuctionExtended({
|
||||
auctionProgramId,
|
||||
resource: settings.resource,
|
||||
}),
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
actions,
|
||||
Metadata,
|
||||
ParsedAccount,
|
||||
WinnerLimit,
|
||||
MasterEdition,
|
||||
SequenceType,
|
||||
sendTransactions,
|
||||
|
@ -20,8 +19,8 @@ import {
|
|||
getSafetyDepositBoxAddress,
|
||||
createAssociatedTokenAccountInstruction,
|
||||
sendTransactionWithRetry,
|
||||
PriceFloor,
|
||||
findProgramAddress,
|
||||
IPartialCreateAuctionArgs,
|
||||
} from '@oyster/common';
|
||||
|
||||
import { AccountLayout, Token } from '@solana/spl-token';
|
||||
|
@ -100,13 +99,10 @@ export async function createAuctionManager(
|
|||
ParsedAccount<WhitelistedCreator>
|
||||
>,
|
||||
settings: AuctionManagerSettings,
|
||||
winnerLimit: WinnerLimit,
|
||||
endAuctionAt: BN,
|
||||
auctionGap: BN,
|
||||
auctionSettings: IPartialCreateAuctionArgs,
|
||||
safetyDepositDrafts: SafetyDepositDraft[],
|
||||
participationSafetyDepositDraft: SafetyDepositDraft | undefined,
|
||||
paymentMint: PublicKey,
|
||||
priceFloor: PriceFloor,
|
||||
): Promise<{
|
||||
vault: PublicKey;
|
||||
auction: PublicKey;
|
||||
|
@ -136,15 +132,7 @@ export async function createAuctionManager(
|
|||
instructions: makeAuctionInstructions,
|
||||
signers: makeAuctionSigners,
|
||||
auction,
|
||||
} = await makeAuction(
|
||||
wallet,
|
||||
winnerLimit,
|
||||
vault,
|
||||
endAuctionAt,
|
||||
auctionGap,
|
||||
paymentMint,
|
||||
priceFloor,
|
||||
);
|
||||
} = await makeAuction(wallet, vault, auctionSettings);
|
||||
|
||||
let safetyDepositConfigsWithPotentiallyUnsetTokens =
|
||||
await buildSafetyDepositArray(
|
||||
|
|
|
@ -2,24 +2,19 @@ import { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js';
|
|||
import {
|
||||
utils,
|
||||
actions,
|
||||
WinnerLimit,
|
||||
PriceFloor,
|
||||
findProgramAddress,
|
||||
IPartialCreateAuctionArgs,
|
||||
CreateAuctionArgs,
|
||||
} from '@oyster/common';
|
||||
|
||||
import BN from 'bn.js';
|
||||
import { METAPLEX_PREFIX } from '../models/metaplex';
|
||||
const { AUCTION_PREFIX, createAuction } = actions;
|
||||
|
||||
// This command makes an auction
|
||||
export async function makeAuction(
|
||||
wallet: any,
|
||||
winnerLimit: WinnerLimit,
|
||||
vault: PublicKey,
|
||||
endAuctionAt: BN,
|
||||
auctionGap: BN,
|
||||
paymentMint: PublicKey,
|
||||
priceFloor: PriceFloor,
|
||||
auctionSettings: IPartialCreateAuctionArgs,
|
||||
): Promise<{
|
||||
auction: PublicKey;
|
||||
instructions: TransactionInstruction[];
|
||||
|
@ -47,17 +42,13 @@ export async function makeAuction(
|
|||
)
|
||||
)[0];
|
||||
|
||||
createAuction(
|
||||
winnerLimit,
|
||||
vault,
|
||||
endAuctionAt,
|
||||
auctionGap,
|
||||
priceFloor,
|
||||
paymentMint,
|
||||
auctionManagerKey,
|
||||
wallet.publicKey,
|
||||
instructions,
|
||||
);
|
||||
const fullSettings = new CreateAuctionArgs({
|
||||
...auctionSettings,
|
||||
authority: auctionManagerKey,
|
||||
resource: vault,
|
||||
});
|
||||
|
||||
createAuction(fullSettings, wallet.publicKey, instructions);
|
||||
|
||||
return { instructions, signers, auction: auctionKey };
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Col, Button, InputNumber, Spin } from 'antd';
|
||||
import { MemoryRouter, Route, Redirect, Link } from 'react-router-dom';
|
||||
|
||||
|
@ -13,6 +13,10 @@ import {
|
|||
formatTokenAmount,
|
||||
useMint,
|
||||
PriceFloorType,
|
||||
AuctionDataExtended,
|
||||
ParsedAccount,
|
||||
getAuctionExtended,
|
||||
programIds,
|
||||
} from '@oyster/common';
|
||||
import { AuctionView, useUserBalance } from '../../hooks';
|
||||
import { sendPlaceBid } from '../../actions/sendPlaceBid';
|
||||
|
@ -26,9 +30,78 @@ import BN from 'bn.js';
|
|||
import { Confetti } from '../Confetti';
|
||||
import { QUOTE_MINT } from '../../constants';
|
||||
import { LAMPORTS_PER_SOL } from '@solana/web3.js';
|
||||
import { useMeta } from '../../contexts';
|
||||
import moment from 'moment';
|
||||
|
||||
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 = ({
|
||||
auctionView,
|
||||
style,
|
||||
|
@ -74,9 +147,21 @@ export const AuctionCard = ({
|
|||
winnerIndex,
|
||||
auctionView,
|
||||
);
|
||||
const auctionExtended = useAuctionExtended(auctionView);
|
||||
|
||||
const eligibleForAnything = winnerIndex !== null || eligibleForOpenEdition;
|
||||
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 (
|
||||
<div className="auction-container" style={style}>
|
||||
|
@ -270,13 +355,32 @@ export const AuctionCard = ({
|
|||
}}
|
||||
>
|
||||
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>
|
||||
)}
|
||||
<br />
|
||||
<AuctionNumbers auctionView={auctionView} />
|
||||
|
||||
<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
|
||||
style={{
|
||||
|
@ -333,6 +437,8 @@ export const AuctionCard = ({
|
|||
className="action-btn"
|
||||
onClick={placeBid}
|
||||
disabled={
|
||||
tickSizeInvalid ||
|
||||
gapBidInvalid ||
|
||||
!myPayingAccount ||
|
||||
value === undefined ||
|
||||
value * LAMPORTS_PER_SOL < priceFloor ||
|
||||
|
|
|
@ -28,6 +28,9 @@ import {
|
|||
setProgramIds,
|
||||
useConnectionConfig,
|
||||
useWallet,
|
||||
AuctionDataExtended,
|
||||
MAX_AUCTION_DATA_EXTENDED_SIZE,
|
||||
AuctionDataExtendedParser,
|
||||
} from '@oyster/common';
|
||||
import { MintInfo } from '@solana/spl-token';
|
||||
import { Connection, PublicKey, PublicKeyAndAccount } from '@solana/web3.js';
|
||||
|
@ -67,6 +70,7 @@ interface MetaState {
|
|||
masterEditionsByOneTimeAuthMint: Record<string, ParsedAccount<MasterEdition>>;
|
||||
auctionManagersByAuction: Record<string, ParsedAccount<AuctionManager>>;
|
||||
auctions: Record<string, ParsedAccount<AuctionData>>;
|
||||
auctionDataExtended: Record<string, ParsedAccount<AuctionDataExtended>>;
|
||||
vaults: Record<string, ParsedAccount<Vault>>;
|
||||
store: ParsedAccount<Store> | null;
|
||||
bidderMetadataByAuctionAndBidder: Record<
|
||||
|
@ -88,29 +92,38 @@ interface MetaState {
|
|||
|
||||
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 {
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const isMetadataPartOfStore = (m: ParsedAccount<Metadata> , store: ParsedAccount<Store> | null, whitelistedCreatorsByCreator: Record<
|
||||
string,
|
||||
ParsedAccount<WhitelistedCreator>
|
||||
>) => {
|
||||
if(!m?.info?.data?.creators) {
|
||||
const isMetadataPartOfStore = (
|
||||
m: ParsedAccount<Metadata>,
|
||||
store: ParsedAccount<Store> | null,
|
||||
whitelistedCreatorsByCreator: Record<
|
||||
string,
|
||||
ParsedAccount<WhitelistedCreator>
|
||||
>,
|
||||
) => {
|
||||
if (!m?.info?.data?.creators) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m.info.data.creators.findIndex(
|
||||
return (
|
||||
m.info.data.creators.findIndex(
|
||||
c =>
|
||||
c.verified &&
|
||||
store &&
|
||||
store.info &&
|
||||
(store.info.public ||
|
||||
whitelistedCreatorsByCreator[c.address.toBase58()]?.info
|
||||
?.activated),
|
||||
) >= 0;
|
||||
}
|
||||
whitelistedCreatorsByCreator[c.address.toBase58()]?.info?.activated),
|
||||
) >= 0
|
||||
);
|
||||
};
|
||||
|
||||
const MetaContext = React.createContext<MetaContextState>({
|
||||
metadata: [],
|
||||
|
@ -122,6 +135,7 @@ const MetaContext = React.createContext<MetaContextState>({
|
|||
editions: {},
|
||||
auctionManagersByAuction: {},
|
||||
auctions: {},
|
||||
auctionDataExtended: {},
|
||||
vaults: {},
|
||||
store: null,
|
||||
isLoading: false,
|
||||
|
@ -141,13 +155,20 @@ export function MetaProvider({ children = null as any }) {
|
|||
metadata: [] as Array<ParsedAccount<Metadata>>,
|
||||
metadataByMint: {} as Record<string, ParsedAccount<Metadata>>,
|
||||
masterEditions: {} as Record<string, ParsedAccount<MasterEdition>>,
|
||||
masterEditionsByPrintingMint: {} as Record<string, ParsedAccount<MasterEdition>>,
|
||||
masterEditionsByOneTimeAuthMint: {} as Record<string, ParsedAccount<MasterEdition>>,
|
||||
masterEditionsByPrintingMint: {} as Record<
|
||||
string,
|
||||
ParsedAccount<MasterEdition>
|
||||
>,
|
||||
masterEditionsByOneTimeAuthMint: {} as Record<
|
||||
string,
|
||||
ParsedAccount<MasterEdition>
|
||||
>,
|
||||
metadataByMasterEdition: {} as any,
|
||||
editions: {},
|
||||
auctionManagersByAuction: {},
|
||||
bidRedemptions: {},
|
||||
auctions: {},
|
||||
auctionDataExtended: {},
|
||||
vaults: {},
|
||||
payoutTickets: {},
|
||||
store: null as ParsedAccount<Store> | null,
|
||||
|
@ -155,7 +176,7 @@ export function MetaProvider({ children = null as any }) {
|
|||
bidderMetadataByAuctionAndBidder: {},
|
||||
bidderPotsByAuctionAndBidder: {},
|
||||
safetyDepositBoxesByVaultAndIndex: {},
|
||||
})
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
|
@ -163,11 +184,11 @@ export function MetaProvider({ children = null as any }) {
|
|||
async metadataByMint => {
|
||||
try {
|
||||
const m = await queryExtendedMetadata(connection, metadataByMint);
|
||||
setState((current) => ({
|
||||
setState(current => ({
|
||||
...current,
|
||||
metadata: m.metadata,
|
||||
metadataByMint: m.mintToMetadata,
|
||||
}))
|
||||
}));
|
||||
} catch (er) {
|
||||
console.error(er);
|
||||
}
|
||||
|
@ -203,6 +224,7 @@ export function MetaProvider({ children = null as any }) {
|
|||
auctionManagersByAuction: {},
|
||||
bidRedemptions: {},
|
||||
auctions: {},
|
||||
auctionDataExtended: {},
|
||||
vaults: {},
|
||||
payoutTickets: {},
|
||||
store: null,
|
||||
|
@ -215,11 +237,11 @@ export function MetaProvider({ children = null as any }) {
|
|||
const updateTemp = (prop: keyof MetaState, key: string, value: any) => {
|
||||
if (prop === 'store') {
|
||||
tempCache[prop] = value;
|
||||
} else if(tempCache[prop]) {
|
||||
} else if (tempCache[prop]) {
|
||||
const bucket = tempCache[prop] as any;
|
||||
bucket[key] = value as any;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (let i = 0; i < accounts.length; i++) {
|
||||
let account = accounts[i];
|
||||
|
@ -227,18 +249,25 @@ export function MetaProvider({ children = null as any }) {
|
|||
processAuctions(account, updateTemp);
|
||||
processMetaData(account, updateTemp);
|
||||
|
||||
await processMetaplexAccounts(
|
||||
account,
|
||||
updateTemp,
|
||||
);
|
||||
await processMetaplexAccounts(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++) {
|
||||
const metadata = values[i];
|
||||
if(isMetadataPartOfStore(metadata, tempCache.store, tempCache.whitelistedCreatorsByCreator)) {
|
||||
if (
|
||||
isMetadataPartOfStore(
|
||||
metadata,
|
||||
tempCache.store,
|
||||
tempCache.whitelistedCreatorsByCreator,
|
||||
)
|
||||
) {
|
||||
await metadata.info.init();
|
||||
tempCache.metadataByMasterEdition[metadata.info?.masterEdition?.toBase58() || ''] = metadata;
|
||||
tempCache.metadataByMasterEdition[
|
||||
metadata.info?.masterEdition?.toBase58() || ''
|
||||
] = metadata;
|
||||
} else {
|
||||
delete tempCache.metadataByMint[metadata.info.mint.toBase58() || ''];
|
||||
}
|
||||
|
@ -248,7 +277,7 @@ export function MetaProvider({ children = null as any }) {
|
|||
tempCache.metadata = values;
|
||||
setState({
|
||||
...tempCache,
|
||||
})
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
console.log('------->set finished');
|
||||
|
@ -259,36 +288,34 @@ export function MetaProvider({ children = null as any }) {
|
|||
return () => {
|
||||
dispose();
|
||||
};
|
||||
}, [
|
||||
connection,
|
||||
setState,
|
||||
updateMints,
|
||||
env,
|
||||
]);
|
||||
}, [connection, setState, updateMints, env]);
|
||||
|
||||
const updateStateValue = useMemo(() => (prop: keyof MetaState, key: string, value: any) => {
|
||||
setState((current) => {
|
||||
if (prop === 'store') {
|
||||
return {
|
||||
...current,
|
||||
[prop]: value,
|
||||
const updateStateValue = useMemo(
|
||||
() => (prop: keyof MetaState, key: string, value: any) => {
|
||||
setState(current => {
|
||||
if (prop === 'store') {
|
||||
return {
|
||||
...current,
|
||||
[prop]: value,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...current,
|
||||
[prop]: {
|
||||
...current[prop],
|
||||
[key]: value,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return ({
|
||||
...current,
|
||||
[prop]: {
|
||||
...current[prop],
|
||||
[key]: value
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [setState]);
|
||||
});
|
||||
},
|
||||
[setState],
|
||||
);
|
||||
|
||||
const store = state.store;
|
||||
const whitelistedCreatorsByCreator = state.whitelistedCreatorsByCreator;
|
||||
useEffect(() => {
|
||||
if(isLoading) {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -341,9 +368,16 @@ export function MetaProvider({ children = null as any }) {
|
|||
updateStateValue,
|
||||
);
|
||||
|
||||
if(result && isMetadataPartOfStore(result, store, whitelistedCreatorsByCreator)) {
|
||||
if (
|
||||
result &&
|
||||
isMetadataPartOfStore(result, store, whitelistedCreatorsByCreator)
|
||||
) {
|
||||
await result.info.init();
|
||||
updateStateValue('metadataByMasterEdition', result.info.masterEdition?.toBase58() || '', result);
|
||||
updateStateValue(
|
||||
'metadataByMasterEdition',
|
||||
result.info.masterEdition?.toBase58() || '',
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: BL
|
||||
|
@ -366,7 +400,7 @@ export function MetaProvider({ children = null as any }) {
|
|||
pubkey,
|
||||
account: info.accountInfo,
|
||||
},
|
||||
updateStateValue
|
||||
updateStateValue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -425,9 +459,12 @@ export function MetaProvider({ children = null as any }) {
|
|||
masterEditions: state.masterEditions,
|
||||
auctionManagersByAuction: state.auctionManagersByAuction,
|
||||
auctions: state.auctions,
|
||||
auctionDataExtended: state.auctionDataExtended,
|
||||
metadataByMint: state.metadataByMint,
|
||||
safetyDepositBoxesByVaultAndIndex: state.safetyDepositBoxesByVaultAndIndex,
|
||||
bidderMetadataByAuctionAndBidder: state.bidderMetadataByAuctionAndBidder,
|
||||
safetyDepositBoxesByVaultAndIndex:
|
||||
state.safetyDepositBoxesByVaultAndIndex,
|
||||
bidderMetadataByAuctionAndBidder:
|
||||
state.bidderMetadataByAuctionAndBidder,
|
||||
bidderPotsByAuctionAndBidder: state.bidderPotsByAuctionAndBidder,
|
||||
vaults: state.vaults,
|
||||
bidRedemptions: state.bidRedemptions,
|
||||
|
@ -520,6 +557,21 @@ const processAuctions = (
|
|||
// ignore errors
|
||||
// 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 {
|
||||
if (a.account.data.length === BIDDER_METADATA_LEN) {
|
||||
const account = cache.add(
|
||||
|
@ -531,9 +583,10 @@ const processAuctions = (
|
|||
setter(
|
||||
'bidderMetadataByAuctionAndBidder',
|
||||
account.info.auctionPubkey.toBase58() +
|
||||
'-' +
|
||||
account.info.bidderPubkey.toBase58(),
|
||||
account);
|
||||
'-' +
|
||||
account.info.bidderPubkey.toBase58(),
|
||||
account,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// ignore errors
|
||||
|
@ -550,9 +603,10 @@ const processAuctions = (
|
|||
setter(
|
||||
'bidderPotsByAuctionAndBidder',
|
||||
account.info.auctionAct.toBase58() +
|
||||
'-' +
|
||||
account.info.bidderAct.toBase58(),
|
||||
account);
|
||||
'-' +
|
||||
account.info.bidderAct.toBase58(),
|
||||
account,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// ignore errors
|
||||
|
@ -586,7 +640,11 @@ const processMetaplexAccounts = async (
|
|||
account: a.account,
|
||||
info: auctionManager,
|
||||
};
|
||||
setter('auctionManagersByAuction', auctionManager.auction.toBase58(), account);
|
||||
setter(
|
||||
'auctionManagersByAuction',
|
||||
auctionManager.auction.toBase58(),
|
||||
account,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (a.account.data[0] === MetaplexKey.BidRedemptionTicketV1) {
|
||||
|
@ -640,7 +698,11 @@ const processMetaplexAccounts = async (
|
|||
account.info.image = nameInfo.image;
|
||||
account.info.twitter = nameInfo.twitter;
|
||||
}
|
||||
setter('whitelistedCreatorsByCreator', whitelistedCreator.address.toBase58(), account);
|
||||
setter(
|
||||
'whitelistedCreatorsByCreator',
|
||||
whitelistedCreator.address.toBase58(),
|
||||
account,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
@ -653,7 +715,8 @@ const processMetaData = (
|
|||
meta: PublicKeyAndAccount<Buffer>,
|
||||
setter: UpdateStateValueFunc,
|
||||
) => {
|
||||
if (meta.account.owner.toBase58() !== programIds().metadata.toBase58()) return;
|
||||
if (meta.account.owner.toBase58() !== programIds().metadata.toBase58())
|
||||
return;
|
||||
|
||||
try {
|
||||
if (meta.account.data[0] === MetadataKey.MetadataV1) {
|
||||
|
@ -687,8 +750,16 @@ const processMetaData = (
|
|||
info: masterEdition,
|
||||
};
|
||||
setter('masterEditions', meta.pubkey.toBase58(), account);
|
||||
setter('masterEditionsByPrintingMint', masterEdition.printingMint.toBase58(), account);
|
||||
setter('masterEditionsByOneTimeAuthMint', masterEdition.oneTimePrintingAuthorizationMint.toBase58(), account);
|
||||
setter(
|
||||
'masterEditionsByPrintingMint',
|
||||
masterEdition.printingMint.toBase58(),
|
||||
account,
|
||||
);
|
||||
setter(
|
||||
'masterEditionsByOneTimeAuthMint',
|
||||
masterEdition.oneTimePrintingAuthorizationMint.toBase58(),
|
||||
account,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// ignore errors
|
||||
|
@ -712,7 +783,8 @@ const processVaultData = (
|
|||
setter(
|
||||
'safetyDepositBoxesByVaultAndIndex',
|
||||
safetyDeposit.vault.toBase58() + '-' + safetyDeposit.order,
|
||||
account);
|
||||
account,
|
||||
);
|
||||
} else if (a.account.data[0] === VaultKey.VaultV1) {
|
||||
const vault = decodeVault(a.account.data);
|
||||
const account: ParsedAccount<Vault> = {
|
||||
|
@ -721,10 +793,7 @@ const processVaultData = (
|
|||
info: vault,
|
||||
};
|
||||
|
||||
setter(
|
||||
'vaults',
|
||||
a.pubkey.toBase58(),
|
||||
account);
|
||||
setter('vaults', a.pubkey.toBase58(), account);
|
||||
}
|
||||
} catch {
|
||||
// ignore errors
|
||||
|
|
|
@ -30,13 +30,9 @@ import {
|
|||
Creator,
|
||||
PriceFloor,
|
||||
PriceFloorType,
|
||||
IPartialCreateAuctionArgs,
|
||||
} from '@oyster/common';
|
||||
import {
|
||||
Connection,
|
||||
LAMPORTS_PER_SOL,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
} from '@solana/web3.js';
|
||||
import { Connection, LAMPORTS_PER_SOL, PublicKey } from '@solana/web3.js';
|
||||
import { MintLayout } from '@solana/spl-token';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { capitalize } from 'lodash';
|
||||
|
@ -306,14 +302,29 @@ export const AuctionCreateView = () => {
|
|||
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(
|
||||
connection,
|
||||
wallet,
|
||||
whitelistedCreatorsByCreator,
|
||||
settings,
|
||||
winnerLimit,
|
||||
new BN((attributes.auctionDuration || 0) * 60), // endAuctionAt is actually auction duration, poorly named, in seconds
|
||||
new BN((attributes.gapTime || 0) * 60),
|
||||
auctionSettings,
|
||||
attributes.category === AuctionCategory.Open
|
||||
? []
|
||||
: attributes.category !== AuctionCategory.Tiered
|
||||
|
@ -323,12 +334,6 @@ export const AuctionCreateView = () => {
|
|||
? attributes.items[0]
|
||||
: attributes.participationNFT,
|
||||
QUOTE_MINT,
|
||||
new PriceFloor({
|
||||
type: attributes.priceFloor
|
||||
? PriceFloorType.Minimum
|
||||
: PriceFloorType.None,
|
||||
minPrice: new BN((attributes.priceFloor || 0) * LAMPORTS_PER_SOL),
|
||||
}),
|
||||
);
|
||||
setAuctionObj(_auctionObj);
|
||||
};
|
||||
|
|
|
@ -122,6 +122,18 @@ pub enum AuctionError {
|
|||
/// Data type mismatch
|
||||
#[error("Data type mismatch")]
|
||||
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 {
|
||||
|
|
|
@ -134,10 +134,10 @@ impl AuctionData {
|
|||
(Some(end), Some(gap)) => {
|
||||
// Check if the bid is within the gap between the last bidder.
|
||||
if let Some(last) = self.last_bid {
|
||||
let next_bid_time = match last.checked_add(gap) {
|
||||
Some(val) => val,
|
||||
None => return Err(AuctionError::NumericalOverflowError.into()),
|
||||
};
|
||||
let next_bid_time = last
|
||||
.checked_add(gap)
|
||||
.ok_or(AuctionError::NumericalOverflowError)?;
|
||||
|
||||
Ok(now > end && now > next_bid_time)
|
||||
} else {
|
||||
Ok(now > end)
|
||||
|
@ -179,6 +179,28 @@ impl AuctionData {
|
|||
};
|
||||
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.
|
||||
|
@ -259,59 +281,120 @@ impl BidState {
|
|||
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
|
||||
/// 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 {
|
||||
// In a capped auction, track the limited number of winners.
|
||||
BidState::EnglishAuction { ref mut bids, max } => match bids.last() {
|
||||
Some(top) => {
|
||||
msg!("Looking to go over the loop");
|
||||
for i in (0..bids.len()).rev() {
|
||||
msg!("Comparison of {:?} and {:?} for {:?}", bids[i].1, bid.1, i);
|
||||
if bids[i].1 < bid.1 {
|
||||
msg!("Ok we can do an insert");
|
||||
if i + 1 < bids.len() {
|
||||
msg!("Doing a normal insert");
|
||||
bids.insert(i + 1, bid);
|
||||
} else {
|
||||
msg!("Doing an on the end insert");
|
||||
bids.push(bid)
|
||||
}
|
||||
break;
|
||||
} else if bids[i].1 == bid.1 {
|
||||
msg!("Ok we can do an equivalent insert");
|
||||
if i == 0 {
|
||||
msg!("Doing a normal insert");
|
||||
BidState::EnglishAuction { ref mut bids, max } => {
|
||||
match bids.last() {
|
||||
Some(top) => {
|
||||
msg!("Looking to go over the loop, but check tick size first");
|
||||
|
||||
for i in (0..bids.len()).rev() {
|
||||
msg!("Comparison of {:?} and {:?} for {:?}", bids[i].1, bid.1, i);
|
||||
if bids[i].1 < bid.1 {
|
||||
if let Some(gap_tick) = gap_tick_size_percentage {
|
||||
BidState::assert_valid_gap_insertion(gap_tick, &bids[i], &bid)?
|
||||
}
|
||||
|
||||
msg!("Ok we can do an insert");
|
||||
if i + 1 < bids.len() {
|
||||
msg!("Doing a normal insert");
|
||||
bids.insert(i + 1, bid);
|
||||
} else {
|
||||
msg!("Doing an on the end 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);
|
||||
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 {
|
||||
bids.remove(0);
|
||||
if bids.len() > max_size {
|
||||
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.
|
||||
BidState::OpenEdition { bids, max } => Ok(()),
|
||||
|
|
|
@ -40,6 +40,10 @@ pub struct CreateAuctionArgs {
|
|||
pub resource: Pubkey,
|
||||
/// Set a price floor.
|
||||
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> {
|
||||
|
@ -98,6 +102,12 @@ pub fn create_auction(
|
|||
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_or_allocate_account_raw(
|
||||
*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.
|
||||
AuctionData {
|
||||
authority: args.authority,
|
||||
|
|
|
@ -316,9 +316,12 @@ pub fn place_bid<'r, 'b: 'r>(
|
|||
|
||||
// Serialize new Auction State
|
||||
auction.last_bid = Some(clock.unix_timestamp);
|
||||
auction
|
||||
.bid_state
|
||||
.place_bid(Bid(*accounts.bidder.key, args.amount))?;
|
||||
auction.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())?;
|
||||
|
||||
// Update latest metadata with results from the bid.
|
||||
|
|
Loading…
Reference in New Issue