Merge branch 'master' of https://github.com/asghaier76/metaplex
This commit is contained in:
commit
788d20e899
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -388,6 +431,10 @@ class CancelBidArgs {
|
|||
}
|
||||
}
|
||||
|
||||
class SetAuthorityArgs {
|
||||
instruction: number = 5;
|
||||
}
|
||||
|
||||
export const AUCTION_SCHEMA = new Map<any, any>([
|
||||
[
|
||||
CreateAuctionArgs,
|
||||
|
@ -402,6 +449,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' }],
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -446,6 +495,14 @@ export const AUCTION_SCHEMA = new Map<any, any>([
|
|||
],
|
||||
},
|
||||
],
|
||||
|
||||
[
|
||||
SetAuthorityArgs,
|
||||
{
|
||||
kind: 'struct',
|
||||
fields: [['instruction', 'u8']],
|
||||
},
|
||||
],
|
||||
[
|
||||
AuctionData,
|
||||
{
|
||||
|
@ -541,39 +598,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 +629,10 @@ export async function createAuction(
|
|||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: await getAuctionExtended({ auctionProgramId, resource }),
|
||||
pubkey: await getAuctionExtended({
|
||||
auctionProgramId,
|
||||
resource: settings.resource,
|
||||
}),
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
|
@ -668,6 +709,42 @@ export async function startAuction(
|
|||
);
|
||||
}
|
||||
|
||||
export async function setAuctionAuthority(
|
||||
auction: PublicKey,
|
||||
currentAuthority: PublicKey,
|
||||
newAuthority: PublicKey,
|
||||
instructions: TransactionInstruction[],
|
||||
) {
|
||||
const auctionProgramId = programIds().auction;
|
||||
|
||||
const data = Buffer.from(serialize(AUCTION_SCHEMA, new SetAuthorityArgs()));
|
||||
|
||||
const keys = [
|
||||
{
|
||||
pubkey: auction,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: currentAuthority,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: newAuthority,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
instructions.push(
|
||||
new TransactionInstruction({
|
||||
keys,
|
||||
programId: auctionProgramId,
|
||||
data: data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function placeBid(
|
||||
bidderPubkey: PublicKey,
|
||||
bidderTokenPubkey: PublicKey,
|
||||
|
|
|
@ -51,7 +51,12 @@ export enum MetadataCategory {
|
|||
VR = 'vr',
|
||||
}
|
||||
|
||||
type FileOrString = File | string;
|
||||
export type MetadataFile = {
|
||||
uri: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type FileOrString = MetadataFile | string;
|
||||
|
||||
export interface IMetadataExtension {
|
||||
name: string;
|
||||
|
@ -61,6 +66,8 @@ export interface IMetadataExtension {
|
|||
description: string;
|
||||
// preview image absolute URI
|
||||
image: string;
|
||||
animation_url?: string;
|
||||
|
||||
// stores link to item on meta
|
||||
external_url: string;
|
||||
|
||||
|
@ -72,7 +79,6 @@ export interface IMetadataExtension {
|
|||
maxSupply?: number;
|
||||
creators?: {
|
||||
address: string;
|
||||
verified: boolean;
|
||||
shares: number;
|
||||
}[];
|
||||
};
|
||||
|
|
|
@ -275,6 +275,42 @@ export const decodeSafetyDeposit = (buffer: Buffer) => {
|
|||
) as SafetyDepositBox;
|
||||
};
|
||||
|
||||
export async function setVaultAuthority(
|
||||
vault: PublicKey,
|
||||
currentAuthority: PublicKey,
|
||||
newAuthority: PublicKey,
|
||||
instructions: TransactionInstruction[],
|
||||
) {
|
||||
const vaultProgramId = programIds().vault;
|
||||
|
||||
const data = Buffer.from([10]);
|
||||
|
||||
const keys = [
|
||||
{
|
||||
pubkey: vault,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: currentAuthority,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: newAuthority,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
instructions.push(
|
||||
new TransactionInstruction({
|
||||
keys,
|
||||
programId: vaultProgramId,
|
||||
data: data,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export async function initVault(
|
||||
allowFurtherShareCreation: boolean,
|
||||
fractionalMint: PublicKey,
|
||||
|
|
|
@ -24,7 +24,6 @@ export async function closeVault(
|
|||
redeemTreasury: PublicKey,
|
||||
priceMint: PublicKey,
|
||||
externalPriceAccount: PublicKey,
|
||||
setAuthorityToAuctionManager: boolean,
|
||||
): Promise<{
|
||||
instructions: TransactionInstruction[];
|
||||
signers: Keypair[];
|
||||
|
@ -48,13 +47,6 @@ export async function closeVault(
|
|||
)
|
||||
)[0];
|
||||
|
||||
const auctionManagerKey: PublicKey = (
|
||||
await findProgramAddress(
|
||||
[Buffer.from(METAPLEX_PREFIX), auctionKey.toBuffer()],
|
||||
PROGRAM_IDS.metaplex,
|
||||
)
|
||||
)[0];
|
||||
|
||||
await activateVault(
|
||||
new BN(0),
|
||||
vault,
|
||||
|
@ -117,7 +109,7 @@ export async function closeVault(
|
|||
fractionMint,
|
||||
fractionTreasury,
|
||||
redeemTreasury,
|
||||
setAuthorityToAuctionManager ? auctionManagerKey : wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
wallet.publicKey,
|
||||
transferAuthority.publicKey,
|
||||
externalPriceAccount,
|
||||
|
|
|
@ -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';
|
||||
|
@ -49,6 +48,7 @@ import { createExternalPriceAccount } from './createExternalPriceAccount';
|
|||
import { validateParticipation } from '../models/metaplex/validateParticipation';
|
||||
import { createReservationListForTokens } from './createReservationListsForTokens';
|
||||
import { populatePrintingTokens } from './populatePrintingTokens';
|
||||
import { setVaultAndAuctionAuthorities } from './setVaultAndAuctionAuthorities';
|
||||
const { createTokenAccount } = actions;
|
||||
|
||||
interface normalPattern {
|
||||
|
@ -73,6 +73,7 @@ interface byType {
|
|||
makeAuction: normalPattern;
|
||||
initAuctionManager: normalPattern;
|
||||
startAuction: normalPattern;
|
||||
setVaultAndAuctionAuthorities: normalPattern;
|
||||
externalPriceAccount: normalPattern;
|
||||
validateParticipation?: normalPattern;
|
||||
buildAndPopulateOneTimeAuthorizationAccount?: normalPattern;
|
||||
|
@ -100,13 +101,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 +134,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(
|
||||
|
@ -212,7 +202,6 @@ export async function createAuctionManager(
|
|||
redeemTreasury,
|
||||
priceMint,
|
||||
externalPriceAccount,
|
||||
true,
|
||||
),
|
||||
addTokens: { instructions: addTokenInstructions, signers: addTokenSigners },
|
||||
createReservationList: {
|
||||
|
@ -227,6 +216,12 @@ export async function createAuctionManager(
|
|||
instructions: auctionManagerInstructions,
|
||||
signers: auctionManagerSigners,
|
||||
},
|
||||
setVaultAndAuctionAuthorities: await setVaultAndAuctionAuthorities(
|
||||
wallet,
|
||||
vault,
|
||||
auction,
|
||||
auctionManager,
|
||||
),
|
||||
startAuction: await setupStartAuction(wallet, vault),
|
||||
validateParticipation: participationSafetyDepositDraft
|
||||
? await validateParticipationHelper(
|
||||
|
@ -277,6 +272,7 @@ export async function createAuctionManager(
|
|||
lookup.closeVault.signers,
|
||||
lookup.makeAuction.signers,
|
||||
lookup.initAuctionManager.signers,
|
||||
lookup.setVaultAndAuctionAuthorities.signers,
|
||||
lookup.validateParticipation?.signers || [],
|
||||
...lookup.validateBoxes.signers,
|
||||
lookup.startAuction.signers,
|
||||
|
@ -292,6 +288,7 @@ export async function createAuctionManager(
|
|||
lookup.closeVault.instructions,
|
||||
lookup.makeAuction.instructions,
|
||||
lookup.initAuctionManager.instructions,
|
||||
lookup.setVaultAndAuctionAuthorities.instructions,
|
||||
lookup.validateParticipation?.instructions || [],
|
||||
...lookup.validateBoxes.instructions,
|
||||
lookup.startAuction.instructions,
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { Keypair, Connection, TransactionInstruction } from '@solana/web3.js';
|
||||
import { sendTransactionsWithManualRetry, TokenAccount } from '@oyster/common';
|
||||
import {
|
||||
sendTransactionsWithManualRetry,
|
||||
setAuctionAuthority,
|
||||
setVaultAuthority,
|
||||
TokenAccount,
|
||||
} from '@oyster/common';
|
||||
|
||||
import { AuctionView } from '../hooks';
|
||||
import { AuctionManagerStatus } from '../models/metaplex';
|
||||
|
@ -21,10 +26,28 @@ export async function decommAuctionManagerAndReturnPrizes(
|
|||
) {
|
||||
let decomSigners: Keypair[] = [];
|
||||
let decomInstructions: TransactionInstruction[] = [];
|
||||
|
||||
if (auctionView.auction.info.authority.equals(wallet.publicKey)) {
|
||||
await setAuctionAuthority(
|
||||
auctionView.auction.pubkey,
|
||||
wallet.publicKey,
|
||||
auctionView.auctionManager.pubkey,
|
||||
decomInstructions,
|
||||
);
|
||||
}
|
||||
if (auctionView.vault.info.authority.equals(wallet.publicKey)) {
|
||||
await setVaultAuthority(
|
||||
auctionView.vault.pubkey,
|
||||
wallet.publicKey,
|
||||
auctionView.auctionManager.pubkey,
|
||||
decomInstructions,
|
||||
);
|
||||
}
|
||||
await decommissionAuctionManager(
|
||||
auctionView.auctionManager.pubkey,
|
||||
auctionView.auction.pubkey,
|
||||
wallet.publicKey,
|
||||
auctionView.vault.pubkey,
|
||||
decomInstructions,
|
||||
);
|
||||
signers.push(decomSigners);
|
||||
|
|
|
@ -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[];
|
||||
|
@ -40,24 +35,13 @@ export async function makeAuction(
|
|||
)
|
||||
)[0];
|
||||
|
||||
const auctionManagerKey: PublicKey = (
|
||||
await findProgramAddress(
|
||||
[Buffer.from(METAPLEX_PREFIX), auctionKey.toBuffer()],
|
||||
PROGRAM_IDS.metaplex,
|
||||
)
|
||||
)[0];
|
||||
const fullSettings = new CreateAuctionArgs({
|
||||
...auctionSettings,
|
||||
authority: wallet.publicKey,
|
||||
resource: vault,
|
||||
});
|
||||
|
||||
createAuction(
|
||||
winnerLimit,
|
||||
vault,
|
||||
endAuctionAt,
|
||||
auctionGap,
|
||||
priceFloor,
|
||||
paymentMint,
|
||||
auctionManagerKey,
|
||||
wallet.publicKey,
|
||||
instructions,
|
||||
);
|
||||
createAuction(fullSettings, wallet.publicKey, instructions);
|
||||
|
||||
return { instructions, signers, auction: auctionKey };
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
Data,
|
||||
Creator,
|
||||
findProgramAddress,
|
||||
MetadataCategory,
|
||||
} from '@oyster/common';
|
||||
import React from 'react';
|
||||
import { MintLayout, Token } from '@solana/spl-token';
|
||||
|
@ -48,6 +49,7 @@ export const mintNFT = async (
|
|||
symbol: string;
|
||||
description: string;
|
||||
image: string | undefined;
|
||||
animation_url: string | undefined;
|
||||
external_url: string;
|
||||
properties: any;
|
||||
creators: Creator[] | null;
|
||||
|
@ -60,28 +62,31 @@ export const mintNFT = async (
|
|||
if (!wallet?.publicKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataContent = {
|
||||
name: metadata.name,
|
||||
symbol: metadata.symbol,
|
||||
description: metadata.description,
|
||||
seller_fee_basis_points: metadata.sellerFeeBasisPoints,
|
||||
image: metadata.image,
|
||||
animation_url: metadata.animation_url,
|
||||
external_url: metadata.external_url,
|
||||
properties: {
|
||||
...metadata.properties,
|
||||
creators: metadata.creators?.map(creator => {
|
||||
return {
|
||||
address: creator.address.toBase58(),
|
||||
share: creator.share,
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const realFiles: File[] = [
|
||||
...files,
|
||||
new File(
|
||||
[
|
||||
JSON.stringify({
|
||||
name: metadata.name,
|
||||
symbol: metadata.symbol,
|
||||
description: metadata.description,
|
||||
seller_fee_basis_points: metadata.sellerFeeBasisPoints,
|
||||
image: metadata.image,
|
||||
external_url: metadata.external_url,
|
||||
properties: {
|
||||
...metadata.properties,
|
||||
creators: metadata.creators?.map(creator => {
|
||||
return {
|
||||
address: creator.address.toBase58(),
|
||||
verified: creator.verified,
|
||||
share: creator.share,
|
||||
};
|
||||
}),
|
||||
},
|
||||
}),
|
||||
JSON.stringify(metadataContent),
|
||||
],
|
||||
'metadata.json',
|
||||
),
|
||||
|
@ -139,22 +144,11 @@ export const mintNFT = async (
|
|||
mintKey,
|
||||
);
|
||||
|
||||
instructions.push(
|
||||
Token.createMintToInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
mintKey,
|
||||
recipientKey,
|
||||
payerPublicKey,
|
||||
[],
|
||||
1,
|
||||
),
|
||||
);
|
||||
|
||||
const metadataAccount = await createMetadata(
|
||||
new Data({
|
||||
symbol: metadata.symbol,
|
||||
name: metadata.name,
|
||||
uri: `https://-------.---/rfX69WKd7Bin_RTbcnH4wM3BuWWsR_ZhWSSqZBLYdMY`,
|
||||
uri: ' '.repeat(64), // size of url for arweave
|
||||
sellerFeeBasisPoints: metadata.sellerFeeBasisPoints,
|
||||
creators: metadata.creators,
|
||||
}),
|
||||
|
@ -212,8 +206,8 @@ export const mintNFT = async (
|
|||
await fetch(
|
||||
// TODO: add CNAME
|
||||
env.startsWith('mainnet-beta')
|
||||
? 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFileProd-1'
|
||||
: 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFile-1',
|
||||
? 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFileProd2'
|
||||
: 'https://us-central1-principal-lane-200702.cloudfunctions.net/uploadFile2',
|
||||
{
|
||||
method: 'POST',
|
||||
body: data,
|
||||
|
@ -246,7 +240,18 @@ export const mintNFT = async (
|
|||
metadataAccount,
|
||||
);
|
||||
|
||||
// // This mint, which allows limited editions to be made, stays with user's wallet.
|
||||
updateInstructions.push(
|
||||
Token.createMintToInstruction(
|
||||
TOKEN_PROGRAM_ID,
|
||||
mintKey,
|
||||
recipientKey,
|
||||
payerPublicKey,
|
||||
[],
|
||||
1,
|
||||
),
|
||||
);
|
||||
|
||||
// This mint, which allows limited editions to be made, stays with user's wallet.
|
||||
const printingMint = createMint(
|
||||
updateInstructions,
|
||||
payerPublicKey,
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js';
|
||||
import { setAuctionAuthority, setVaultAuthority } from '@oyster/common';
|
||||
|
||||
// This command sets the authorities on the vault and auction to be the newly created auction manager.
|
||||
export async function setVaultAndAuctionAuthorities(
|
||||
wallet: any,
|
||||
vault: PublicKey,
|
||||
auction: PublicKey,
|
||||
auctionManager: PublicKey,
|
||||
): Promise<{
|
||||
instructions: TransactionInstruction[];
|
||||
signers: Keypair[];
|
||||
}> {
|
||||
let signers: Keypair[] = [];
|
||||
let instructions: TransactionInstruction[] = [];
|
||||
|
||||
await setAuctionAuthority(
|
||||
auction,
|
||||
wallet.publicKey,
|
||||
auctionManager,
|
||||
instructions,
|
||||
);
|
||||
await setVaultAuthority(
|
||||
vault,
|
||||
wallet.publicKey,
|
||||
auctionManager,
|
||||
instructions,
|
||||
);
|
||||
|
||||
return { instructions, signers };
|
||||
}
|
|
@ -53,7 +53,6 @@ export async function unwindVault(
|
|||
vault.info.redeemTreasury,
|
||||
decoded.priceMint,
|
||||
vault.info.pricingLookupAddress,
|
||||
false,
|
||||
);
|
||||
|
||||
signers.push(cvSigners);
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
padding: 24px 24px 0 24px;
|
||||
min-height: 120px;
|
||||
min-height: 55px;
|
||||
}
|
||||
|
||||
.ant-avatar.ant-avatar-circle {
|
||||
|
|
|
@ -12,9 +12,12 @@ const { Meta } = Card;
|
|||
|
||||
export interface ArtCardProps extends CardProps {
|
||||
pubkey?: PublicKey;
|
||||
|
||||
image?: string;
|
||||
file?: string;
|
||||
animationURL?: string;
|
||||
|
||||
category?: MetadataCategory;
|
||||
|
||||
name?: string;
|
||||
symbol?: string;
|
||||
description?: string;
|
||||
|
@ -22,7 +25,7 @@ export interface ArtCardProps extends CardProps {
|
|||
preview?: boolean;
|
||||
small?: boolean;
|
||||
close?: () => void;
|
||||
endAuctionAt?: number;
|
||||
|
||||
height?: number;
|
||||
width?: number;
|
||||
}
|
||||
|
@ -33,7 +36,7 @@ export const ArtCard = (props: ArtCardProps) => {
|
|||
small,
|
||||
category,
|
||||
image,
|
||||
file,
|
||||
animationURL,
|
||||
name,
|
||||
preview,
|
||||
creators,
|
||||
|
@ -79,8 +82,8 @@ export const ArtCard = (props: ArtCardProps) => {
|
|||
<ArtContent
|
||||
pubkey={pubkey}
|
||||
|
||||
extension={file || image}
|
||||
uri={image}
|
||||
animationURL={animationURL}
|
||||
category={category}
|
||||
|
||||
preview={preview}
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
import React, { Ref, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Image } from 'antd';
|
||||
import { MetadataCategory } from '@oyster/common';
|
||||
import { MetadataCategory, MetadataFile } from '@oyster/common';
|
||||
import { MeshViewer } from '../MeshViewer';
|
||||
import { ThreeDots } from '../MyLoader';
|
||||
import { useCachedImage, useExtendedArt } from '../../hooks';
|
||||
import { Stream, StreamPlayerApi } from '@cloudflare/stream-react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { getLast } from '../../utils/utils';
|
||||
|
||||
const MeshArtContent = ({
|
||||
uri,
|
||||
animationUrl,
|
||||
className,
|
||||
style,
|
||||
files,
|
||||
}: {
|
||||
uri?: string;
|
||||
animationUrl?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
files?: string[];
|
||||
files?: (MetadataFile | string)[];
|
||||
}) => {
|
||||
const renderURL = files && files.length > 0 ? files[0] : uri;
|
||||
const renderURL = files && files.length > 0 && typeof files[0] === 'string' ? files[0] : animationUrl;
|
||||
const { isLoading } = useCachedImage(renderURL || '', true);
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -62,16 +64,18 @@ const CachedImageContent = ({
|
|||
}
|
||||
|
||||
const VideoArtContent = ({
|
||||
extension,
|
||||
className,
|
||||
style,
|
||||
files,
|
||||
uri,
|
||||
animationURL,
|
||||
active,
|
||||
}: {
|
||||
extension?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
files?: string[];
|
||||
files?: (MetadataFile | string)[];
|
||||
uri?: string;
|
||||
animationURL?: string;
|
||||
active?: boolean;
|
||||
}) => {
|
||||
const [playerApi, setPlayerApi] = useState<StreamPlayerApi>();
|
||||
|
@ -90,9 +94,13 @@ const VideoArtContent = ({
|
|||
}, [active, playerApi]);
|
||||
|
||||
const likelyVideo = (files || []).filter((f, index, arr) => {
|
||||
if(typeof f !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: filter by fileType
|
||||
return arr.length >= 2 ? index === 1 : index === 0;
|
||||
})[0];
|
||||
})?.[0] as string;
|
||||
|
||||
const content = (
|
||||
likelyVideo && likelyVideo.startsWith('https://watch.videodelivery.net/') ? (
|
||||
|
@ -116,15 +124,17 @@ const VideoArtContent = ({
|
|||
<video
|
||||
className={className}
|
||||
playsInline={true}
|
||||
autoPlay={false}
|
||||
autoPlay={true}
|
||||
muted={true}
|
||||
controls={true}
|
||||
controlsList="nodownload"
|
||||
style={style}
|
||||
loop={true}
|
||||
poster={extension}
|
||||
poster={uri}
|
||||
>
|
||||
<source src={likelyVideo} type="video/mp4" style={style} />
|
||||
{likelyVideo && <source src={likelyVideo} type="video/mp4" style={style} />}
|
||||
{animationURL && <source src={animationURL} type="video/mp4" style={style} />}
|
||||
{files?.filter(f => typeof f !== 'string').map((f: any) => <source src={f.uri} type={f.type} style={style} />)}
|
||||
</video>
|
||||
)
|
||||
);
|
||||
|
@ -145,7 +155,7 @@ export const ArtContent = ({
|
|||
pubkey,
|
||||
|
||||
uri,
|
||||
extension,
|
||||
animationURL,
|
||||
files,
|
||||
}: {
|
||||
category?: MetadataCategory;
|
||||
|
@ -158,24 +168,29 @@ export const ArtContent = ({
|
|||
active?: boolean;
|
||||
allowMeshRender?: boolean;
|
||||
pubkey?: PublicKey | string,
|
||||
|
||||
extension?: string;
|
||||
uri?: string;
|
||||
files?: string[];
|
||||
animationURL?: string;
|
||||
files?: (MetadataFile | string)[];
|
||||
}) => {
|
||||
const id = typeof pubkey === 'string' ? pubkey : pubkey?.toBase58() || '';
|
||||
|
||||
const { ref, data } = useExtendedArt(id);
|
||||
|
||||
if(pubkey && data) {
|
||||
files = data.properties.files?.filter(f => typeof f === 'string') as string[];
|
||||
files = data.properties.files;
|
||||
uri = data.image;
|
||||
animationURL = data.animation_url;
|
||||
category = data.properties.category;
|
||||
}
|
||||
|
||||
if (allowMeshRender&& (extension?.endsWith('.glb') || category === 'vr')) {
|
||||
animationURL = animationURL || '';
|
||||
|
||||
const animationUrlExt = new URLSearchParams(getLast(animationURL.split("?"))).get("ext");
|
||||
|
||||
if (allowMeshRender && (category === 'vr' || animationUrlExt === 'glb' || animationUrlExt === 'gltf')) {
|
||||
return <MeshArtContent
|
||||
uri={uri}
|
||||
animationUrl={animationURL}
|
||||
className={className}
|
||||
style={style}
|
||||
files={files}/>;
|
||||
|
@ -183,10 +198,11 @@ export const ArtContent = ({
|
|||
|
||||
const content = category === 'video' ? (
|
||||
<VideoArtContent
|
||||
extension={extension}
|
||||
className={className}
|
||||
style={style}
|
||||
files={files}
|
||||
uri={uri}
|
||||
animationURL={animationURL}
|
||||
active={active}
|
||||
/>
|
||||
) : (
|
||||
|
|
|
@ -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 ||
|
||||
|
|
|
@ -92,7 +92,7 @@
|
|||
font-weight: 600;
|
||||
padding: 24px 24px 0px 24px;
|
||||
white-space: normal !important;
|
||||
min-height: 120px;
|
||||
min-height: 55px;
|
||||
}
|
||||
|
||||
.ant-avatar.ant-avatar-circle,
|
||||
|
|
|
@ -11,10 +11,7 @@ import {
|
|||
useWallet,
|
||||
VaultState,
|
||||
} from '@oyster/common';
|
||||
import {
|
||||
Connection,
|
||||
PublicKey,
|
||||
} from '@solana/web3.js';
|
||||
import { Connection, PublicKey } from '@solana/web3.js';
|
||||
import { Badge, Popover, List } from 'antd';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
@ -26,10 +23,7 @@ import { settle } from '../../actions/settle';
|
|||
|
||||
import { QUOTE_MINT } from '../../constants';
|
||||
import { useMeta } from '../../contexts';
|
||||
import {
|
||||
AuctionViewState,
|
||||
useAuctions,
|
||||
} from '../../hooks';
|
||||
import { AuctionViewState, useAuctions } from '../../hooks';
|
||||
import './index.less';
|
||||
import { WalletAdapter } from '@solana/wallet-base';
|
||||
interface NotificationCard {
|
||||
|
@ -180,7 +174,7 @@ export function useSettlementAuctions({
|
|||
notifications: NotificationCard[];
|
||||
}) {
|
||||
const { accountByMint } = useUserAccounts();
|
||||
const walletPubkey = wallet?.publicKey?.toBase58() || '';
|
||||
const walletPubkey = wallet?.publicKey;
|
||||
const { bidderPotsByAuctionAndBidder } = useMeta();
|
||||
const auctionsNeedingSettling = useAuctions(AuctionViewState.Ended);
|
||||
|
||||
|
@ -191,7 +185,8 @@ export function useSettlementAuctions({
|
|||
const nextBatch = auctionsNeedingSettling
|
||||
.filter(
|
||||
a =>
|
||||
a.auctionManager.info.authority.toBase58() === walletPubkey &&
|
||||
walletPubkey &&
|
||||
a.auctionManager.info.authority.equals(walletPubkey) &&
|
||||
a.auction.info.ended(),
|
||||
)
|
||||
.sort(
|
||||
|
@ -270,7 +265,6 @@ export function useSettlementAuctions({
|
|||
myPayingAccount?.pubkey,
|
||||
accountByMint,
|
||||
);
|
||||
const PROGRAM_IDS = programIds();
|
||||
if (wallet?.publicKey) {
|
||||
const ata = await getPersonalEscrowAta(wallet);
|
||||
if (ata) await closePersonalEscrow(connection, wallet, ata);
|
||||
|
|
|
@ -6,16 +6,18 @@ import { AuctionView, useArt } from '../../hooks';
|
|||
import { ArtContent } from '../ArtContent';
|
||||
import { AuctionCard } from '../AuctionCard';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useMeta } from '../../contexts';
|
||||
|
||||
interface IPreSaleBanner {
|
||||
auction?: AuctionView;
|
||||
}
|
||||
|
||||
export const PreSaleBanner = ({ auction }: IPreSaleBanner) => {
|
||||
const { isLoading } = useMeta();
|
||||
const id = auction?.thumbnail.metadata.pubkey;
|
||||
const art = useArt();
|
||||
|
||||
if (!auction) {
|
||||
if (isLoading) {
|
||||
return <Skeleton />;
|
||||
}
|
||||
|
||||
|
@ -31,7 +33,7 @@ export const PreSaleBanner = ({ auction }: IPreSaleBanner) => {
|
|||
<h2 className="art-title">
|
||||
{art.title}
|
||||
</h2>
|
||||
<AuctionCard
|
||||
{auction && <AuctionCard
|
||||
auctionView={auction}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
|
@ -54,7 +56,7 @@ export const PreSaleBanner = ({ auction }: IPreSaleBanner) => {
|
|||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
/>}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
|
|
@ -22,5 +22,12 @@
|
|||
"image": "https://pbs.twimg.com/profile_images/1393353972371623938/ZMWvvptg_400x400.jpg",
|
||||
"description": "",
|
||||
"background": ""
|
||||
},
|
||||
"SoL351y4uKWtbH14AU1Rhiao96aBM4u57bMi5Vj2XJc": {
|
||||
"name": "Solana",
|
||||
"image": "https://pbs.twimg.com/profile_images/1299400345144049665/sPxnVXa7_400x400.jpg",
|
||||
"description": "Account used by Solana to mint official NFTs.",
|
||||
"background": ""
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,9 @@ import {
|
|||
Vault,
|
||||
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 +69,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 +91,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 +134,7 @@ const MetaContext = React.createContext<MetaContextState>({
|
|||
editions: {},
|
||||
auctionManagersByAuction: {},
|
||||
auctions: {},
|
||||
auctionDataExtended: {},
|
||||
vaults: {},
|
||||
store: null,
|
||||
isLoading: false,
|
||||
|
@ -141,13 +154,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 +175,7 @@ export function MetaProvider({ children = null as any }) {
|
|||
bidderMetadataByAuctionAndBidder: {},
|
||||
bidderPotsByAuctionAndBidder: {},
|
||||
safetyDepositBoxesByVaultAndIndex: {},
|
||||
})
|
||||
});
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
|
@ -163,11 +183,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 +223,7 @@ export function MetaProvider({ children = null as any }) {
|
|||
auctionManagersByAuction: {},
|
||||
bidRedemptions: {},
|
||||
auctions: {},
|
||||
auctionDataExtended: {},
|
||||
vaults: {},
|
||||
payoutTickets: {},
|
||||
store: null,
|
||||
|
@ -215,11 +236,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 +248,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 +276,7 @@ export function MetaProvider({ children = null as any }) {
|
|||
tempCache.metadata = values;
|
||||
setState({
|
||||
...tempCache,
|
||||
})
|
||||
});
|
||||
|
||||
setIsLoading(false);
|
||||
console.log('------->set finished');
|
||||
|
@ -259,36 +287,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,16 +367,24 @@ 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);
|
||||
setState((data) => ({
|
||||
...data,
|
||||
metadata: [...data.metadata.filter(m => m.pubkey.equals(pubkey)), result],
|
||||
metadataByMasterEdition: {
|
||||
...data.metadataByMasterEdition,
|
||||
[result.info.masterEdition?.toBase58() || '']: result,
|
||||
},
|
||||
metadataByMint: {
|
||||
...data.metadataByMint,
|
||||
[result.info.mint.toBase58()]: result
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// TODO: BL
|
||||
// setMetadataByMint(latest => {
|
||||
// updateMints(latest);
|
||||
// return latest;
|
||||
// });
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -366,7 +400,7 @@ export function MetaProvider({ children = null as any }) {
|
|||
pubkey,
|
||||
account: info.accountInfo,
|
||||
},
|
||||
updateStateValue
|
||||
updateStateValue,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -380,6 +414,7 @@ export function MetaProvider({ children = null as any }) {
|
|||
}, [
|
||||
connection,
|
||||
updateStateValue,
|
||||
setState,
|
||||
updateMints,
|
||||
store,
|
||||
whitelistedCreatorsByCreator,
|
||||
|
@ -425,9 +460,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,
|
||||
|
@ -466,7 +504,7 @@ const queryExtendedMetadata = async (
|
|||
MintParser,
|
||||
false,
|
||||
) as ParsedAccount<MintInfo>;
|
||||
if (mint.info.supply.gt(new BN(1)) || mint.info.decimals !== 0) {
|
||||
if (!mint.info.supply.eqn(1) || mint.info.decimals !== 0) {
|
||||
// naive not NFT check
|
||||
delete mintToMetadata[key];
|
||||
} else {
|
||||
|
@ -520,6 +558,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 +584,10 @@ const processAuctions = (
|
|||
setter(
|
||||
'bidderMetadataByAuctionAndBidder',
|
||||
account.info.auctionPubkey.toBase58() +
|
||||
'-' +
|
||||
account.info.bidderPubkey.toBase58(),
|
||||
account);
|
||||
'-' +
|
||||
account.info.bidderPubkey.toBase58(),
|
||||
account,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// ignore errors
|
||||
|
@ -550,9 +604,10 @@ const processAuctions = (
|
|||
setter(
|
||||
'bidderPotsByAuctionAndBidder',
|
||||
account.info.auctionAct.toBase58() +
|
||||
'-' +
|
||||
account.info.bidderAct.toBase58(),
|
||||
account);
|
||||
'-' +
|
||||
account.info.bidderAct.toBase58(),
|
||||
account,
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// ignore errors
|
||||
|
@ -586,7 +641,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 +699,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 +716,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 +751,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 +784,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 +794,7 @@ const processVaultData = (
|
|||
info: vault,
|
||||
};
|
||||
|
||||
setter(
|
||||
'vaults',
|
||||
a.pubkey.toBase58(),
|
||||
account);
|
||||
setter('vaults', a.pubkey.toBase58(), account);
|
||||
}
|
||||
} catch {
|
||||
// ignore errors
|
||||
|
|
|
@ -12,6 +12,7 @@ export async function decommissionAuctionManager(
|
|||
auctionManager: PublicKey,
|
||||
auction: PublicKey,
|
||||
authority: PublicKey,
|
||||
vault: PublicKey,
|
||||
instructions: TransactionInstruction[],
|
||||
) {
|
||||
const PROGRAM_IDS = programIds();
|
||||
|
@ -41,6 +42,11 @@ export async function decommissionAuctionManager(
|
|||
isWritable: false,
|
||||
},
|
||||
|
||||
{
|
||||
pubkey: vault,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: store,
|
||||
isSigner: false,
|
||||
|
|
|
@ -1,3 +1,15 @@
|
|||
export const cleanName = (name: string): string => {
|
||||
export const cleanName = (name?: string): string | undefined => {
|
||||
if (!name) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return name.replaceAll(' ', '-');
|
||||
};
|
||||
|
||||
export const getLast = <T>(arr: T[]) => {
|
||||
if (arr.length <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return arr[arr.length - 1];
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useArt, useExtendedArt } from './../../hooks';
|
|||
|
||||
import './index.less';
|
||||
import { ArtContent } from '../../components/ArtContent';
|
||||
import { shortenAddress, useConnection, useWallet } from '@oyster/common';
|
||||
import { shortenAddress, TokenAccount, useConnection, useUserAccounts, useWallet } from '@oyster/common';
|
||||
import { MetaAvatar } from '../../components/MetaAvatar';
|
||||
import { sendSignMetadata } from '../../actions/sendSignMetadata';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
|
@ -15,10 +15,18 @@ const { Content } = Layout;
|
|||
export const ArtView = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { wallet } = useWallet();
|
||||
|
||||
const connection = useConnection();
|
||||
const art = useArt(id);
|
||||
const { ref, data } = useExtendedArt(id);
|
||||
|
||||
// const { userAccounts } = useUserAccounts();
|
||||
|
||||
// const accountByMint = userAccounts.reduce((prev, acc) => {
|
||||
// prev.set(acc.info.mint.toBase58(), acc);
|
||||
// return prev;
|
||||
// }, new Map<string, TokenAccount>());
|
||||
|
||||
const description = data?.description;
|
||||
|
||||
const pubkey = wallet?.publicKey?.toBase58() || '';
|
||||
|
@ -55,6 +63,7 @@ export const ArtView = () => {
|
|||
className="artwork-image"
|
||||
pubkey={id}
|
||||
active={true}
|
||||
allowMeshRender={true}
|
||||
/>
|
||||
</Col>
|
||||
{/* <Divider /> */}
|
||||
|
@ -79,6 +88,31 @@ export const ArtView = () => {
|
|||
<span className="creator-name">
|
||||
{creator.name || shortenAddress(creator.address || '')}
|
||||
</span>
|
||||
{/* <Button
|
||||
onClick={async () => {
|
||||
if(!art.mint) {
|
||||
return;
|
||||
}
|
||||
const mint = new PublicKey(art.mint);
|
||||
|
||||
const account = accountByMint.get(art.mint);
|
||||
if(!account) {
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = wallet?.publicKey;
|
||||
|
||||
if(!owner) {
|
||||
return;
|
||||
}
|
||||
const instructions: any[] = [];
|
||||
await updateMetadata(undefined, undefined, true, mint, owner, instructions)
|
||||
|
||||
sendTransaction(connection, wallet, instructions, [], true);
|
||||
}}
|
||||
>
|
||||
Mark as Sold
|
||||
</Button> */}
|
||||
<div style={{ marginLeft: 10 }}>
|
||||
{!creator.verified &&
|
||||
(creator.address === pubkey ? (
|
||||
|
|
|
@ -30,12 +30,13 @@ import {
|
|||
shortenAddress,
|
||||
MetaplexModal,
|
||||
MetaplexOverlay,
|
||||
MetadataFile,
|
||||
} from '@oyster/common';
|
||||
import { getAssetCostToStore, LAMPORT_MULTIPLIER } from '../../utils/assets';
|
||||
import { Connection, PublicKey } from '@solana/web3.js';
|
||||
import { MintLayout } from '@solana/spl-token';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import { cleanName } from '../../utils/utils';
|
||||
import { cleanName, getLast } from '../../utils/utils';
|
||||
import { AmountLabel } from '../../components/AmountLabel';
|
||||
import useWindowDimensions from '../../utils/layout';
|
||||
|
||||
|
@ -56,12 +57,14 @@ export const ArtCreateView = () => {
|
|||
const [progress, setProgress] = useState<number>(0);
|
||||
const [nft, setNft] =
|
||||
useState<{ metadataAccount: PublicKey } | undefined>(undefined);
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [attributes, setAttributes] = useState<IMetadataExtension>({
|
||||
name: '',
|
||||
symbol: '',
|
||||
description: '',
|
||||
external_url: '',
|
||||
image: '',
|
||||
animation_url: undefined,
|
||||
seller_fee_basis_points: 0,
|
||||
creators: [],
|
||||
properties: {
|
||||
|
@ -85,22 +88,17 @@ export const ArtCreateView = () => {
|
|||
|
||||
// store files
|
||||
const mint = async () => {
|
||||
const fileNames = (attributes?.properties?.files || []).map(f =>
|
||||
typeof f === 'string' ? f : f.name,
|
||||
);
|
||||
const files = (attributes?.properties?.files || []).filter(
|
||||
f => typeof f !== 'string',
|
||||
) as File[];
|
||||
const metadata = {
|
||||
name: attributes.name,
|
||||
symbol: attributes.symbol,
|
||||
creators: attributes.creators,
|
||||
description: attributes.description,
|
||||
sellerFeeBasisPoints: attributes.seller_fee_basis_points,
|
||||
image: fileNames && fileNames?.[0] && fileNames[0],
|
||||
image: attributes.image,
|
||||
animation_url: attributes.animation_url,
|
||||
external_url: attributes.external_url,
|
||||
properties: {
|
||||
files: fileNames,
|
||||
files: attributes.properties.files,
|
||||
category: attributes.properties?.category,
|
||||
},
|
||||
};
|
||||
|
@ -165,6 +163,8 @@ export const ArtCreateView = () => {
|
|||
<UploadStep
|
||||
attributes={attributes}
|
||||
setAttributes={setAttributes}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
confirm={() => gotoStep(2)}
|
||||
/>
|
||||
)}
|
||||
|
@ -172,6 +172,7 @@ export const ArtCreateView = () => {
|
|||
{step === 2 && (
|
||||
<InfoStep
|
||||
attributes={attributes}
|
||||
files={files}
|
||||
setAttributes={setAttributes}
|
||||
confirm={() => gotoStep(3)}
|
||||
/>
|
||||
|
@ -186,6 +187,7 @@ export const ArtCreateView = () => {
|
|||
{step === 4 && (
|
||||
<LaunchStep
|
||||
attributes={attributes}
|
||||
files={files}
|
||||
confirm={() => gotoStep(5)}
|
||||
connection={connection}
|
||||
/>
|
||||
|
@ -283,14 +285,16 @@ const CategoryStep = (props: {
|
|||
const UploadStep = (props: {
|
||||
attributes: IMetadataExtension;
|
||||
setAttributes: (attr: IMetadataExtension) => void;
|
||||
files: File[],
|
||||
setFiles: (files: File[]) => void,
|
||||
confirm: () => void;
|
||||
}) => {
|
||||
const [mainFile, setMainFile] = useState<any>();
|
||||
const [coverFile, setCoverFile] = useState<any>();
|
||||
const [image, setImage] = useState<string>('');
|
||||
const [imageURL, setImageURL] = useState<string>('');
|
||||
const [imageURLErr, setImageURLErr] = useState<string>('');
|
||||
const disableContinue = (!mainFile && !image) || !!imageURLErr;
|
||||
const [coverFile, setCoverFile] = useState<File | undefined>(props.files?.[0]);
|
||||
const [mainFile, setMainFile] = useState<File | undefined>(props.files?.[1]);
|
||||
|
||||
const [customURL, setCustomURL] = useState<string>('');
|
||||
const [customURLErr, setCustomURLErr] = useState<string>('');
|
||||
const disableContinue = (!coverFile) || !!customURLErr;
|
||||
|
||||
useEffect(() => {
|
||||
props.setAttributes({
|
||||
|
@ -344,46 +348,63 @@ const UploadStep = (props: {
|
|||
very first time.
|
||||
</p>
|
||||
</Row>
|
||||
<Row className="content-action" style={{ marginBottom: 5 }}>
|
||||
<h3>{uploadMsg(props.attributes.properties?.category)}</h3>
|
||||
<Dragger
|
||||
accept={acceptableFiles(props.attributes.properties?.category)}
|
||||
style={{ padding: 20, background: 'rgba(255, 255, 255, 0.08)' }}
|
||||
multiple={false}
|
||||
customRequest={info => {
|
||||
// dont upload files here, handled outside of the control
|
||||
info?.onSuccess?.({}, null as any);
|
||||
}}
|
||||
fileList={mainFile ? [mainFile] : []}
|
||||
onChange={async info => {
|
||||
const file = info.file.originFileObj;
|
||||
<Row className="content-action">
|
||||
<h3>
|
||||
Upload a cover image (PNG, JPG, GIF)
|
||||
</h3>
|
||||
<Dragger
|
||||
accept=".png,.jpg,.gif,.mp4"
|
||||
style={{ padding: 20 }}
|
||||
multiple={false}
|
||||
customRequest={info => {
|
||||
// dont upload files here, handled outside of the control
|
||||
info?.onSuccess?.({}, null as any);
|
||||
}}
|
||||
fileList={coverFile ? [coverFile as any] : []}
|
||||
onChange={async info => {
|
||||
const file = info.file.originFileObj;
|
||||
if (file) setCoverFile(file);
|
||||
}}
|
||||
>
|
||||
<div className="ant-upload-drag-icon">
|
||||
<h3 style={{ fontWeight: 700 }}>
|
||||
Upload your cover image (PNG, JPG, GIF)
|
||||
</h3>
|
||||
</div>
|
||||
<p className="ant-upload-text">Drag and drop, or click to browse</p>
|
||||
</Dragger>
|
||||
</Row>
|
||||
{(props.attributes.properties?.category !== MetadataCategory.Image) && (
|
||||
<Row className="content-action" style={{ marginBottom: 5, marginTop: 30 }}>
|
||||
<h3>{uploadMsg(props.attributes.properties?.category)}</h3>
|
||||
<Dragger
|
||||
accept={acceptableFiles(props.attributes.properties?.category)}
|
||||
style={{ padding: 20, background: 'rgba(255, 255, 255, 0.08)' }}
|
||||
multiple={false}
|
||||
customRequest={info => {
|
||||
// dont upload files here, handled outside of the control
|
||||
info?.onSuccess?.({}, null as any);
|
||||
}}
|
||||
fileList={mainFile ? [mainFile as any] : []}
|
||||
onChange={async info => {
|
||||
const file = info.file.originFileObj;
|
||||
|
||||
// Reset image URL
|
||||
setImageURL('');
|
||||
setImageURLErr('');
|
||||
// Reset image URL
|
||||
setCustomURL('');
|
||||
setCustomURLErr('');
|
||||
|
||||
if (file) setMainFile(file);
|
||||
if (
|
||||
props.attributes.properties?.category !== MetadataCategory.Audio
|
||||
) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
setImage((event.target?.result as string) || '');
|
||||
};
|
||||
if (file) reader.readAsDataURL(file);
|
||||
}
|
||||
}}
|
||||
onRemove={() => {
|
||||
setMainFile(null);
|
||||
setImage('');
|
||||
}}
|
||||
>
|
||||
<div className="ant-upload-drag-icon">
|
||||
<h3 style={{ fontWeight: 700 }}>Upload your creation</h3>
|
||||
</div>
|
||||
<p className="ant-upload-text">Drag and drop, or click to browse</p>
|
||||
</Dragger>
|
||||
</Row>
|
||||
if (file) setMainFile(file);
|
||||
}}
|
||||
onRemove={() => {
|
||||
setMainFile(undefined);
|
||||
}}
|
||||
>
|
||||
<div className="ant-upload-drag-icon">
|
||||
<h3 style={{ fontWeight: 700 }}>Upload your creation</h3>
|
||||
</div>
|
||||
<p className="ant-upload-text">Drag and drop, or click to browse</p>
|
||||
</Dragger>
|
||||
</Row>)}
|
||||
<Form.Item
|
||||
style={{
|
||||
width: '100%',
|
||||
|
@ -394,72 +415,33 @@ const UploadStep = (props: {
|
|||
label={<h3>OR use absolute URL to content</h3>}
|
||||
labelAlign="left"
|
||||
colon={false}
|
||||
validateStatus={imageURLErr ? 'error' : 'success'}
|
||||
help={imageURLErr}
|
||||
validateStatus={customURLErr ? 'error' : 'success'}
|
||||
help={customURLErr}
|
||||
>
|
||||
<Input
|
||||
disabled={!!mainFile}
|
||||
placeholder="http://example.com/path/to/image"
|
||||
value={imageURL}
|
||||
onChange={ev => setImageURL(ev.target.value)}
|
||||
onFocus={() => setImageURLErr('')}
|
||||
value={customURL}
|
||||
onChange={ev => setCustomURL(ev.target.value)}
|
||||
onFocus={() => setCustomURLErr('')}
|
||||
onBlur={() => {
|
||||
if (!imageURL) {
|
||||
setImageURLErr('');
|
||||
if (!customURL) {
|
||||
setCustomURLErr('');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate URL and save
|
||||
new URL(imageURL);
|
||||
setImage(imageURL);
|
||||
new URL(customURL);
|
||||
setCustomURL(customURL);
|
||||
setCustomURLErr('');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setImageURLErr('Please enter a valid absolute URL');
|
||||
setCustomURLErr('Please enter a valid absolute URL');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
{props.attributes.properties?.category === MetadataCategory.Audio && (
|
||||
<Row className="content-action">
|
||||
<h3>
|
||||
Optionally, you can upload a cover image or video (PNG, JPG, GIF,
|
||||
MP4)
|
||||
</h3>
|
||||
<Dragger
|
||||
accept=".png,.jpg,.gif,.mp4"
|
||||
style={{ padding: 20 }}
|
||||
multiple={false}
|
||||
customRequest={info => {
|
||||
// dont upload files here, handled outside of the control
|
||||
info?.onSuccess?.({}, null as any);
|
||||
}}
|
||||
fileList={coverFile ? [coverFile] : []}
|
||||
onChange={async info => {
|
||||
const file = info.file.originFileObj;
|
||||
if (file) setCoverFile(file);
|
||||
if (
|
||||
props.attributes.properties?.category === MetadataCategory.Audio
|
||||
) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
setImage((event.target?.result as string) || '');
|
||||
};
|
||||
if (file) reader.readAsDataURL(file);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="ant-upload-drag-icon">
|
||||
<h3 style={{ fontWeight: 700 }}>
|
||||
Upload your cover image or video (PNG, JPG, GIF, MP4)
|
||||
</h3>
|
||||
</div>
|
||||
<p className="ant-upload-text">Drag and drop, or click to browse</p>
|
||||
</Dragger>
|
||||
<h3 style={{ marginTop: 30 }}>OR use absolute URL to content</h3>
|
||||
<Input />
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Button
|
||||
type="primary"
|
||||
|
@ -470,16 +452,24 @@ const UploadStep = (props: {
|
|||
...props.attributes,
|
||||
properties: {
|
||||
...props.attributes.properties,
|
||||
files: imageURL
|
||||
? [imageURL]
|
||||
: [mainFile, coverFile]
|
||||
files: [coverFile, mainFile, customURL]
|
||||
.filter(f => f)
|
||||
.map(
|
||||
f => new File([f], cleanName(f.name), { type: f.type }),
|
||||
f => {
|
||||
const uri = typeof f === 'string' ? f : (cleanName(f?.name) || '');
|
||||
const type = typeof f === 'string' || !f ? 'unknown' : f.type || (getLast(f.name.split('.')) || 'unknown');
|
||||
|
||||
return ({
|
||||
uri,
|
||||
type
|
||||
}) as MetadataFile;
|
||||
},
|
||||
),
|
||||
},
|
||||
image: imageURL || image,
|
||||
image: cleanName(coverFile?.name) || '',
|
||||
animation_url: cleanName(mainFile && mainFile.name),
|
||||
});
|
||||
props.setFiles([coverFile, mainFile].filter(f => f) as File[]);
|
||||
props.confirm();
|
||||
}}
|
||||
style={{ marginTop: 24 }}
|
||||
|
@ -497,17 +487,55 @@ interface Royalty {
|
|||
amount: number;
|
||||
}
|
||||
|
||||
const useArtworkFiles = (files: File[], attributes: IMetadataExtension) => {
|
||||
const [data, setData] = useState<{ image: string, animation_url: string }>({ image: '', animation_url: '' });
|
||||
|
||||
useEffect(() => {
|
||||
if(attributes.image) {
|
||||
const file = files.find(f => f.name === attributes.image);
|
||||
if(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
setData((data: any) => {
|
||||
return {
|
||||
...(data || {}),
|
||||
image: (event.target?.result as string) || '',
|
||||
}
|
||||
});
|
||||
};
|
||||
if (file) reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
if(attributes.animation_url) {
|
||||
const file = files.find(f => f.name === attributes.animation_url);
|
||||
if(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (event) {
|
||||
setData((data: any) => {
|
||||
return {
|
||||
...(data || {}),
|
||||
animation_url: (event.target?.result as string) || '',
|
||||
}
|
||||
});
|
||||
};
|
||||
if (file) reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
}, [files, attributes]);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const InfoStep = (props: {
|
||||
attributes: IMetadataExtension;
|
||||
files: File[],
|
||||
setAttributes: (attr: IMetadataExtension) => void;
|
||||
confirm: () => void;
|
||||
}) => {
|
||||
const [creators, setCreators] = useState<Array<UserValue>>([]);
|
||||
const [royalties, setRoyalties] = useState<Array<Royalty>>([]);
|
||||
const { wallet } = useWallet();
|
||||
|
||||
const file = props.attributes.properties.files?.[0];
|
||||
const fileName = typeof file === 'string' ? file : file?.name;
|
||||
const { image, animation_url } = useArtworkFiles(props.files, props.attributes);
|
||||
|
||||
useEffect(() => {
|
||||
setRoyalties(
|
||||
|
@ -530,8 +558,8 @@ const InfoStep = (props: {
|
|||
<Col>
|
||||
{props.attributes.image && (
|
||||
<ArtCard
|
||||
image={props.attributes.image}
|
||||
file={fileName || ''}
|
||||
image={image}
|
||||
animationURL={animation_url}
|
||||
category={props.attributes.properties?.category}
|
||||
name={props.attributes.name}
|
||||
symbol={props.attributes.symbol}
|
||||
|
@ -893,19 +921,13 @@ const RoyaltiesStep = (props: {
|
|||
const LaunchStep = (props: {
|
||||
confirm: () => void;
|
||||
attributes: IMetadataExtension;
|
||||
files: File[],
|
||||
connection: Connection;
|
||||
}) => {
|
||||
const files = (props.attributes.properties?.files || []).filter(
|
||||
f => typeof f !== 'string',
|
||||
) as File[];
|
||||
const fileNames = (props.attributes.properties?.files || []).map(f =>
|
||||
typeof f === 'string' ? f : f?.name,
|
||||
);
|
||||
const metadata = {
|
||||
...(props.attributes as any),
|
||||
files: fileNames,
|
||||
};
|
||||
const [cost, setCost] = useState(0);
|
||||
const { image, animation_url } = useArtworkFiles(props.files, props.attributes);
|
||||
const files = props.files;
|
||||
const metadata = props.attributes;
|
||||
useEffect(() => {
|
||||
const rentCall = Promise.all([
|
||||
props.connection.getMinimumBalanceForRentExemption(MintLayout.span),
|
||||
|
@ -932,7 +954,7 @@ const LaunchStep = (props: {
|
|||
// TODO: add fees based on number of transactions and signers
|
||||
setCost(sol + additionalSol);
|
||||
});
|
||||
}, [files, setCost]);
|
||||
}, [files, metadata, setCost]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -947,8 +969,8 @@ const LaunchStep = (props: {
|
|||
<Col>
|
||||
{props.attributes.image && (
|
||||
<ArtCard
|
||||
image={props.attributes.image}
|
||||
file={fileNames?.[0] || ''}
|
||||
image={image}
|
||||
animationURL={animation_url}
|
||||
category={props.attributes.properties?.category}
|
||||
name={props.attributes.name}
|
||||
symbol={props.attributes.symbol}
|
||||
|
|
|
@ -416,6 +416,7 @@ export const InnerBillingView = ({
|
|||
<ArtContent
|
||||
pubkey={id}
|
||||
className="artwork-image"
|
||||
allowMeshRender={true}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
|
|
|
@ -64,6 +64,7 @@ export const AuctionItem = ({
|
|||
className="artwork-image stack-item"
|
||||
style={style}
|
||||
active={active}
|
||||
allowMeshRender={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -30,12 +30,12 @@ import {
|
|||
Creator,
|
||||
PriceFloor,
|
||||
PriceFloorType,
|
||||
IPartialCreateAuctionArgs,
|
||||
} from '@oyster/common';
|
||||
import {
|
||||
Connection,
|
||||
LAMPORTS_PER_SOL,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
} from '@solana/web3.js';
|
||||
import { MintLayout } from '@solana/spl-token';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
|
@ -306,14 +306,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 +338,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.
|
||||
|
|
|
@ -35,6 +35,12 @@ pub fn set_authority(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramRe
|
|||
return Err(AuctionError::InvalidAuthority.into());
|
||||
}
|
||||
|
||||
// Make sure new authority actually exists in some form.
|
||||
if new_authority.data_is_empty() || new_authority.lamports() == 0 {
|
||||
msg!("Disallowing new authority because it does not exist.");
|
||||
return Err(AuctionError::InvalidAuthority.into());
|
||||
}
|
||||
|
||||
auction.authority = *new_authority.key;
|
||||
auction.serialize(&mut *auction_act.data.borrow_mut())?;
|
||||
Ok(())
|
||||
|
|
|
@ -357,9 +357,10 @@ pub enum MetaplexInstruction {
|
|||
/// 0. `[writable]` Auction Manager
|
||||
/// 1. `[writable]` Auction
|
||||
/// 2. `[Signer]` Authority of the Auction Manager
|
||||
/// 3. `[]` Store
|
||||
/// 4. `[]` Auction program
|
||||
/// 5. `[]` Clock sysvar
|
||||
/// 3. `[]` Vault
|
||||
/// 4. `[]` Store
|
||||
/// 5. `[]` Auction program
|
||||
/// 6. `[]` Clock sysvar
|
||||
DecommissionAuctionManager,
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ use {
|
|||
entrypoint::ProgramResult,
|
||||
pubkey::Pubkey,
|
||||
},
|
||||
spl_auction::processor::AuctionData,
|
||||
spl_token_vault::state::Vault,
|
||||
};
|
||||
|
||||
pub fn process_decommission_auction_manager<'a>(
|
||||
|
@ -24,18 +26,29 @@ pub fn process_decommission_auction_manager<'a>(
|
|||
let auction_manager_info = next_account_info(account_info_iter)?;
|
||||
let auction_info = next_account_info(account_info_iter)?;
|
||||
let authority_info = next_account_info(account_info_iter)?;
|
||||
let vault_info = next_account_info(account_info_iter)?;
|
||||
let store_info = next_account_info(account_info_iter)?;
|
||||
let auction_program_info = next_account_info(account_info_iter)?;
|
||||
let clock_info = next_account_info(account_info_iter)?;
|
||||
|
||||
assert_owned_by(auction_manager_info, program_id)?;
|
||||
assert_owned_by(store_info, program_id)?;
|
||||
assert_signer(authority_info)?;
|
||||
|
||||
let mut auction_manager = AuctionManager::from_account_info(auction_manager_info)?;
|
||||
let vault = Vault::from_account_info(vault_info)?;
|
||||
let auction = AuctionData::from_account_info(auction_info)?;
|
||||
|
||||
let store = Store::from_account_info(store_info)?;
|
||||
assert_authority_correct(&auction_manager, authority_info)?;
|
||||
|
||||
if auction.authority != *auction_manager_info.key {
|
||||
return Err(MetaplexError::AuctionAuthorityMismatch.into());
|
||||
}
|
||||
|
||||
if vault.authority != *auction_manager_info.key {
|
||||
return Err(MetaplexError::VaultAuthorityMismatch.into());
|
||||
}
|
||||
|
||||
if auction_manager.state.status != AuctionManagerStatus::Initialized {
|
||||
return Err(MetaplexError::InvalidStatus.into());
|
||||
}
|
||||
|
|
|
@ -53,14 +53,6 @@ pub fn process_init_auction_manager(
|
|||
return Err(MetaplexError::AuctionMustBeCreated.into());
|
||||
}
|
||||
|
||||
if vault.authority != *auction_manager_info.key {
|
||||
return Err(MetaplexError::VaultAuthorityMismatch.into());
|
||||
}
|
||||
|
||||
if auction.authority != *auction_manager_info.key {
|
||||
return Err(MetaplexError::AuctionAuthorityMismatch.into());
|
||||
}
|
||||
|
||||
let bump_seed = assert_derivation(
|
||||
program_id,
|
||||
auction_manager_info,
|
||||
|
|
|
@ -28,16 +28,10 @@ fn set_reservation_list_wrapper<'a>(
|
|||
auction_manager_info: &AccountInfo<'a>,
|
||||
signer_seeds: &[&[u8]],
|
||||
reservations: Vec<Reservation>,
|
||||
first_push: bool,
|
||||
total_reservation_spots: u64,
|
||||
total_reservation_spots: Option<u64>,
|
||||
offset: u64,
|
||||
total_spot_offset: u64,
|
||||
) -> ProgramResult {
|
||||
let total_reservation_spot_opt: Option<u64>;
|
||||
|
||||
if first_push {
|
||||
total_reservation_spot_opt = Some(total_reservation_spots)
|
||||
} else {
|
||||
total_reservation_spot_opt = None
|
||||
}
|
||||
invoke_signed(
|
||||
&set_reservation_list(
|
||||
*program_id,
|
||||
|
@ -45,7 +39,9 @@ fn set_reservation_list_wrapper<'a>(
|
|||
*reservation_list_info.key,
|
||||
*auction_manager_info.key,
|
||||
reservations,
|
||||
total_reservation_spot_opt,
|
||||
total_reservation_spots,
|
||||
offset,
|
||||
total_spot_offset,
|
||||
),
|
||||
&[
|
||||
master_edition_info.clone(),
|
||||
|
@ -58,12 +54,27 @@ fn set_reservation_list_wrapper<'a>(
|
|||
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)]
|
||||
pub fn reserve_list_if_needed<'a>(
|
||||
program_id: &'a Pubkey,
|
||||
auction_manager: &AuctionManager,
|
||||
auction: &AuctionData,
|
||||
winning_config_item: &WinningConfigItem,
|
||||
winning_index: usize,
|
||||
bidder_info: &AccountInfo<'a>,
|
||||
master_edition_info: &AccountInfo<'a>,
|
||||
reservation_list_info: &AccountInfo<'a>,
|
||||
auction_manager_info: &AccountInfo<'a>,
|
||||
|
@ -71,78 +82,56 @@ pub fn reserve_list_if_needed<'a>(
|
|||
) -> ProgramResult {
|
||||
let reservation_list = get_reservation_list(reservation_list_info)?;
|
||||
|
||||
if reservation_list.supply_snapshot().is_none() {
|
||||
let mut reservations: Vec<Reservation> = vec![];
|
||||
let total_reservation_spot_opt: Option<u64>;
|
||||
|
||||
// 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 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
|
||||
// storage does.)
|
||||
let mut total_reservation_spots: u64 = 0;
|
||||
for n in 0..auction_manager.settings.winning_configs.len() {
|
||||
match auction.winner_at(n) {
|
||||
Some(address) => {
|
||||
let spots: 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();
|
||||
total_reservation_spots = total_reservation_spots
|
||||
// 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 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
|
||||
// storage does.)
|
||||
|
||||
let mut total_reservation_spots: u64 = 0;
|
||||
let mut total_spot_offset: u64 = 0;
|
||||
for n in 0..auction_manager.settings.winning_configs.len() {
|
||||
match auction.winner_at(n) {
|
||||
Some(_) => {
|
||||
let spots: u64 = calc_spots(winning_config_item, auction_manager, n);
|
||||
total_reservation_spots = total_reservation_spots
|
||||
.checked_add(spots)
|
||||
.ok_or(MetaplexError::NumericalOverflowError)?;
|
||||
if n < winning_index {
|
||||
total_spot_offset = total_spot_offset
|
||||
.checked_add(spots)
|
||||
.ok_or(MetaplexError::NumericalOverflowError)?;
|
||||
reservations.push(Reservation {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
let mut first_push = true;
|
||||
let mut reservation_queue: Vec<Reservation> = vec![];
|
||||
for reservation in reservations {
|
||||
reservation_queue.push(reservation);
|
||||
if reservation_queue.len().checked_rem(20) == Some(0) && reservation_queue.len() > 0 {
|
||||
set_reservation_list_wrapper(
|
||||
program_id,
|
||||
master_edition_info,
|
||||
reservation_list_info,
|
||||
auction_manager_info,
|
||||
signer_seeds,
|
||||
reservation_queue,
|
||||
first_push,
|
||||
total_reservation_spots,
|
||||
)?;
|
||||
|
||||
first_push = false;
|
||||
reservation_queue = vec![]; // start over with new list.
|
||||
}
|
||||
}
|
||||
|
||||
if reservation_queue.len() > 0 {
|
||||
set_reservation_list_wrapper(
|
||||
program_id,
|
||||
master_edition_info,
|
||||
reservation_list_info,
|
||||
auction_manager_info,
|
||||
signer_seeds,
|
||||
reservation_queue,
|
||||
first_push,
|
||||
total_reservation_spots,
|
||||
)?;
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
pub fn process_redeem_bid<'a>(
|
||||
|
@ -244,6 +233,8 @@ pub fn process_redeem_bid<'a>(
|
|||
&auction_manager,
|
||||
&auction,
|
||||
&winning_config_item,
|
||||
winning_index,
|
||||
bidder_info,
|
||||
master_edition_info,
|
||||
reservation_list_info,
|
||||
auction_manager_info,
|
||||
|
|
|
@ -11,7 +11,10 @@ use {
|
|||
program::invoke_signed,
|
||||
pubkey::Pubkey,
|
||||
},
|
||||
spl_auction::instruction::{start_auction_instruction, StartAuctionArgs},
|
||||
spl_auction::{
|
||||
instruction::{start_auction_instruction, StartAuctionArgs},
|
||||
processor::AuctionData,
|
||||
},
|
||||
};
|
||||
|
||||
pub fn issue_start_auction<'a>(
|
||||
|
@ -45,8 +48,13 @@ pub fn process_start_auction(program_id: &Pubkey, accounts: &[AccountInfo]) -> P
|
|||
let clock_info = next_account_info(account_info_iter)?;
|
||||
|
||||
let mut auction_manager = AuctionManager::from_account_info(auction_manager_info)?;
|
||||
let auction = AuctionData::from_account_info(auction_info)?;
|
||||
let store = Store::from_account_info(store_info)?;
|
||||
|
||||
if auction.authority != *auction_manager_info.key {
|
||||
return Err(MetaplexError::AuctionAuthorityMismatch.into());
|
||||
}
|
||||
|
||||
assert_authority_correct(&auction_manager, authority_info)?;
|
||||
assert_owned_by(auction_info, &store.auction_program)?;
|
||||
assert_owned_by(auction_manager_info, program_id)?;
|
||||
|
|
|
@ -52,6 +52,10 @@ pub fn process_validate_participation(
|
|||
let open_edition_metadata = Metadata::from_account_info(open_edition_metadata_info)?;
|
||||
let master_edition = MasterEdition::from_account_info(open_master_edition_info)?;
|
||||
|
||||
if vault.authority != *auction_manager_info.key {
|
||||
return Err(MetaplexError::VaultAuthorityMismatch.into());
|
||||
}
|
||||
|
||||
// top level authority and ownership check
|
||||
assert_authority_correct(&auction_manager, authority_info)?;
|
||||
assert_owned_by(auction_manager_info, program_id)?;
|
||||
|
|
|
@ -104,10 +104,14 @@ pub fn process_validate_safety_deposit_box(
|
|||
let metadata = Metadata::from_account_info(metadata_info)?;
|
||||
let store = Store::from_account_info(auction_manager_store_info)?;
|
||||
// Is it a real vault?
|
||||
let _vault = Vault::from_account_info(vault_info)?;
|
||||
let vault = Vault::from_account_info(vault_info)?;
|
||||
// Is it a real mint?
|
||||
let _mint: Mint = assert_initialized(mint_info)?;
|
||||
|
||||
if vault.authority != *auction_manager_info.key {
|
||||
return Err(MetaplexError::VaultAuthorityMismatch.into());
|
||||
}
|
||||
|
||||
assert_owned_by(auction_manager_info, program_id)?;
|
||||
assert_owned_by(metadata_info, &store.token_metadata_program)?;
|
||||
if !original_authority_lookup_info.data_is_empty() {
|
||||
|
|
|
@ -274,6 +274,10 @@ pub enum MetadataError {
|
|||
/// 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 {
|
||||
|
|
|
@ -47,6 +47,13 @@ pub struct SetReservationListArgs {
|
|||
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.
|
||||
|
@ -126,13 +133,13 @@ pub enum MetadataInstruction {
|
|||
/// 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.
|
||||
///
|
||||
/// NOTE: If you have more than 30 addresses in a reservation list, this may be called multiple times to build up the list,
|
||||
/// 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'])
|
||||
/// 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),
|
||||
|
||||
/// Create an empty reservation list for a resource who can come back later as a signer and fill the reservation list
|
||||
|
@ -370,6 +377,8 @@ pub fn set_reservation_list(
|
|||
resource: Pubkey,
|
||||
reservations: Vec<Reservation>,
|
||||
total_reservation_spots: Option<u64>,
|
||||
offset: u64,
|
||||
total_spot_offset: u64,
|
||||
) -> Instruction {
|
||||
Instruction {
|
||||
program_id,
|
||||
|
@ -381,6 +390,8 @@ pub fn set_reservation_list(
|
|||
data: MetadataInstruction::SetReservationList(SetReservationListArgs {
|
||||
reservations,
|
||||
total_reservation_spots,
|
||||
offset,
|
||||
total_spot_offset,
|
||||
})
|
||||
.try_to_vec()
|
||||
.unwrap(),
|
||||
|
|
|
@ -75,6 +75,8 @@ pub fn process_instruction(
|
|||
accounts,
|
||||
args.reservations,
|
||||
args.total_reservation_spots,
|
||||
args.offset,
|
||||
args.total_spot_offset,
|
||||
)
|
||||
}
|
||||
MetadataInstruction::CreateReservationList => {
|
||||
|
@ -547,6 +549,8 @@ pub fn process_set_reservation_list(
|
|||
accounts: &[AccountInfo],
|
||||
reservations: Vec<Reservation>,
|
||||
total_reservation_spots: Option<u64>,
|
||||
offset: u64,
|
||||
total_spot_offset: u64,
|
||||
) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
|
||||
|
@ -600,7 +604,7 @@ pub fn process_set_reservation_list(
|
|||
}
|
||||
reservation_list.set_current_reservation_spots(total_len);
|
||||
|
||||
reservation_list.add_reservations(reservations);
|
||||
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));
|
||||
|
|
|
@ -174,8 +174,13 @@ pub trait ReservationList {
|
|||
fn current_reservation_spots(&self) -> u64;
|
||||
fn set_master_edition(&mut self, key: Pubkey);
|
||||
fn set_supply_snapshot(&mut self, supply: Option<u64>);
|
||||
fn set_reservations(&mut self, reservations: Vec<Reservation>);
|
||||
fn add_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;
|
||||
|
@ -231,12 +236,52 @@ impl ReservationList for ReservationListV2 {
|
|||
self.supply_snapshot = supply;
|
||||
}
|
||||
|
||||
fn add_reservations(&mut self, mut reservations: Vec<Reservation>) {
|
||||
self.reservations.append(&mut reservations)
|
||||
fn add_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>) {
|
||||
self.reservations = reservations
|
||||
fn set_reservations(&mut self, reservations: Vec<Reservation>) -> ProgramResult {
|
||||
self.reservations = reservations;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save(&self, account: &AccountInfo) -> ProgramResult {
|
||||
|
@ -322,7 +367,12 @@ impl ReservationList for ReservationListV1 {
|
|||
self.supply_snapshot = supply;
|
||||
}
|
||||
|
||||
fn add_reservations(&mut self, reservations: Vec<Reservation>) {
|
||||
fn add_reservations(
|
||||
&mut self,
|
||||
reservations: Vec<Reservation>,
|
||||
_: u64,
|
||||
_: u64,
|
||||
) -> ProgramResult {
|
||||
self.reservations = reservations
|
||||
.iter()
|
||||
.map(|r| ReservationV1 {
|
||||
|
@ -331,10 +381,13 @@ impl ReservationList for ReservationListV1 {
|
|||
total_spots: r.total_spots as u8,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_reservations(&mut self, reservations: Vec<Reservation>) {
|
||||
self.add_reservations(reservations);
|
||||
fn set_reservations(&mut self, reservations: Vec<Reservation>) -> ProgramResult {
|
||||
self.add_reservations(reservations, 0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save(&self, account: &AccountInfo) -> ProgramResult {
|
||||
|
|
|
@ -424,6 +424,7 @@ pub fn mint_limited_edition<'a>(
|
|||
let mut reservations = reservation_list.reservations();
|
||||
for i in 0..reservations.len() {
|
||||
let mut reservation = &mut reservations[i];
|
||||
|
||||
if reservation.address == *mint_authority_info.key {
|
||||
offset = Some(
|
||||
prev_total_offsets
|
||||
|
@ -436,13 +437,21 @@ pub fn mint_limited_edition<'a>(
|
|||
.checked_sub(1)
|
||||
.ok_or(MetadataError::NumericalOverflowError)?;
|
||||
|
||||
reservation_list.set_reservations(reservations);
|
||||
reservation_list.set_reservations(reservations)?;
|
||||
reservation_list.save(account)?;
|
||||
break;
|
||||
}
|
||||
prev_total_offsets = prev_total_offsets
|
||||
.checked_add(reservation.total_spots)
|
||||
.ok_or(MetadataError::NumericalOverflowError)?;
|
||||
|
||||
if reservation.address == solana_program::system_program::id() {
|
||||
// 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 {
|
||||
|
|
|
@ -104,6 +104,8 @@ fn show_reservation_list(app_matches: &ArgMatches, _payer: Keypair, client: RpcC
|
|||
"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) {
|
||||
|
|
|
@ -140,6 +140,13 @@ pub enum VaultInstruction {
|
|||
/// Useful for testing purposes, and the CLI makes use of it as well so that you can verify logic.
|
||||
/// 0. `[writable]` External price account
|
||||
UpdateExternalPriceAccount(ExternalPriceAccount),
|
||||
|
||||
/// Sets the authority of the vault to a new authority.
|
||||
///
|
||||
/// 0. `[writable]` Vault
|
||||
/// 1. `[signer]` Vault authority
|
||||
/// 2. `[]` New authority
|
||||
SetAuthority,
|
||||
}
|
||||
|
||||
/// Creates an InitVault instruction
|
||||
|
@ -432,3 +439,20 @@ pub fn create_add_shares_instruction(
|
|||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_set_authority_instruction(
|
||||
program_id: Pubkey,
|
||||
vault: Pubkey,
|
||||
current_authority: Pubkey,
|
||||
new_authority: Pubkey,
|
||||
) -> Instruction {
|
||||
Instruction {
|
||||
program_id,
|
||||
accounts: vec![
|
||||
AccountMeta::new(vault, false),
|
||||
AccountMeta::new_readonly(current_authority, true),
|
||||
AccountMeta::new_readonly(new_authority, false),
|
||||
],
|
||||
data: VaultInstruction::SetAuthority.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,10 @@ pub fn process_instruction(
|
|||
args.allowed_to_combine,
|
||||
)
|
||||
}
|
||||
VaultInstruction::SetAuthority => {
|
||||
msg!("Instruction: Set Authority");
|
||||
process_set_authority(program_id, accounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,6 +116,35 @@ pub fn process_update_external_price_account(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_set_authority(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
|
||||
let account_info_iter = &mut accounts.iter();
|
||||
let vault_info = next_account_info(account_info_iter)?;
|
||||
let current_authority_info = next_account_info(account_info_iter)?;
|
||||
let new_authority_info = next_account_info(account_info_iter)?;
|
||||
|
||||
let mut vault = Vault::from_account_info(vault_info)?;
|
||||
assert_owned_by(vault_info, program_id)?;
|
||||
|
||||
if vault.authority != *current_authority_info.key {
|
||||
return Err(VaultError::InvalidAuthority.into());
|
||||
}
|
||||
|
||||
if !current_authority_info.is_signer {
|
||||
return Err(VaultError::InvalidAuthority.into());
|
||||
}
|
||||
|
||||
// Make sure new authority actually exists in some form.
|
||||
if new_authority_info.data_is_empty() || new_authority_info.lamports() == 0 {
|
||||
msg!("Disallowing new authority because it does not exist.");
|
||||
return Err(VaultError::InvalidAuthority.into());
|
||||
}
|
||||
|
||||
vault.authority = *new_authority_info.key;
|
||||
vault.serialize(&mut *vault_info.data.borrow_mut())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_add_fractional_shares_to_treasury(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
|
|
Loading…
Reference in New Issue