diff --git a/packages/common/src/actions/metadata.ts b/packages/common/src/actions/metadata.ts index a5beb99..47482bf 100644 --- a/packages/common/src/actions/metadata.ts +++ b/packages/common/src/actions/metadata.ts @@ -8,6 +8,8 @@ import { programIds } from '../utils/ids'; import { deserializeBorsh } from './../utils/borsh'; import { serialize } from 'borsh'; import BN from 'bn.js'; +import { PublicKeyInput } from 'node:crypto'; +import { ParsedAccount } from '..'; export const METADATA_PREFIX = 'metadata'; export const EDITION = 'edition'; @@ -70,6 +72,20 @@ export class MasterEdition { this.masterMint = args.masterMint; } } + +export class Edition { + key: MetadataKey; + /// Points at MasterEdition struct + parent: PublicKey; + /// Starting at 0 for master record, this is incremented for each edition minted. + edition: BN; + + constructor(args: { key: MetadataKey; parent: PublicKey; edition: BN }) { + this.key = MetadataKey.EditionV1; + this.parent = args.parent; + this.edition = args.edition; + } +} export class Metadata { key: MetadataKey; nonUniqueSpecificUpdateAuthority?: PublicKey; @@ -80,6 +96,9 @@ export class Metadata { uri: string; extended?: IMetadataExtension; + masterEdition?: PublicKey; + edition?: PublicKey; + nameSymbolTuple?: PublicKey; constructor(args: { nonUniqueSpecificUpdateAuthority?: PublicKey; @@ -216,6 +235,17 @@ export const METADATA_SCHEMA = new Map([ ], }, ], + [ + Edition, + { + kind: 'struct', + fields: [ + ['key', 'u8'], + ['parent', 'pubkey'], + ['edition', 'u64'], + ], + }, + ], [ Metadata, { @@ -246,8 +276,36 @@ export const METADATA_SCHEMA = new Map([ ], ]); -export const decodeMetadata = (buffer: Buffer) => { - return deserializeBorsh(METADATA_SCHEMA, Metadata, buffer) as Metadata; +export const decodeMetadata = async (buffer: Buffer): Promise => { + const metadata = deserializeBorsh( + METADATA_SCHEMA, + Metadata, + buffer, + ) as Metadata; + metadata.nameSymbolTuple = await getNameSymbol(metadata); + metadata.edition = await getEdition(metadata.mint); + metadata.masterEdition = await getEdition(metadata.mint); + return metadata; +}; + +export const decodeEdition = (buffer: Buffer) => { + return deserializeBorsh(METADATA_SCHEMA, Edition, buffer) as Edition; +}; + +export const decodeMasterEdition = (buffer: Buffer) => { + return deserializeBorsh( + METADATA_SCHEMA, + MasterEdition, + buffer, + ) as MasterEdition; +}; + +export const decodeNameSymbolTuple = (buffer: Buffer) => { + return deserializeBorsh( + METADATA_SCHEMA, + NameSymbolTuple, + buffer, + ) as NameSymbolTuple; }; export async function transferUpdateAuthority( @@ -571,3 +629,36 @@ export async function createMasterEdition( }), ); } + +export async function getNameSymbol(metadata: Metadata): Promise { + const PROGRAM_IDS = programIds(); + + return ( + await PublicKey.findProgramAddress( + [ + Buffer.from(METADATA_PREFIX), + PROGRAM_IDS.metadata.toBuffer(), + metadata.mint.toBuffer(), + Buffer.from(metadata.name), + Buffer.from(metadata.symbol), + ], + PROGRAM_IDS.metadata, + ) + )[0]; +} + +export async function getEdition(tokenMint: PublicKey): Promise { + const PROGRAM_IDS = programIds(); + + return ( + await PublicKey.findProgramAddress( + [ + Buffer.from(METADATA_PREFIX), + PROGRAM_IDS.metadata.toBuffer(), + tokenMint.toBuffer(), + Buffer.from(EDITION), + ], + PROGRAM_IDS.metadata, + ) + )[0]; +} diff --git a/packages/metavinci/src/actions/createAuctionManager.tsx b/packages/metavinci/src/actions/createAuctionManager.tsx index 2695722..b4e1a29 100644 --- a/packages/metavinci/src/actions/createAuctionManager.tsx +++ b/packages/metavinci/src/actions/createAuctionManager.tsx @@ -14,6 +14,7 @@ import { SequenceType, sendTransactions, getSafetyDepositBox, + Edition, } from '@oyster/common'; import { AccountLayout } from '@solana/spl-token'; @@ -54,8 +55,9 @@ interface byType { export interface SafetyDepositDraft { metadata: ParsedAccount; - nameSymbol: ParsedAccount; - masterEdition: ParsedAccount; + nameSymbol?: ParsedAccount; + masterEdition?: ParsedAccount; + edition?: ParsedAccount; holding: PublicKey; } @@ -222,11 +224,11 @@ async function setupAuctionManagerInstructions( await initAuctionManager( vault, openEditionSafetyDeposit?.metadata.pubkey, - openEditionSafetyDeposit?.nameSymbol.pubkey, + openEditionSafetyDeposit?.nameSymbol?.pubkey, wallet.pubkey, - openEditionSafetyDeposit?.masterEdition.pubkey, + openEditionSafetyDeposit?.masterEdition?.pubkey, openEditionSafetyDeposit?.metadata.info.mint, - openEditionSafetyDeposit?.masterEdition.info.masterMint, + openEditionSafetyDeposit?.masterEdition?.info.masterMint, wallet.pubkey, wallet.pubkey, wallet.pubkey, @@ -276,7 +278,7 @@ async function validateBoxes( await validateSafetyDepositBox( vault, safetyDeposits[i].metadata.pubkey, - safetyDeposits[i].nameSymbol.pubkey, + safetyDeposits[i].nameSymbol?.pubkey, safetyDepositBox, stores[i], safetyDeposits[i].metadata.info.mint, diff --git a/packages/metavinci/src/contexts/meta.tsx b/packages/metavinci/src/contexts/meta.tsx index ae76b6c..9faa693 100644 --- a/packages/metavinci/src/contexts/meta.tsx +++ b/packages/metavinci/src/contexts/meta.tsx @@ -1,39 +1,104 @@ -import { EventEmitter, programIds, useConnection, decodeMetadata, Metadata, getMultipleAccounts, cache, MintParser, ParsedAccount } from '@oyster/common'; +import { + EventEmitter, + programIds, + useConnection, + decodeMetadata, + decodeNameSymbolTuple, + decodeEdition, + decodeMasterEdition, + Metadata, + getMultipleAccounts, + cache, + MintParser, + ParsedAccount, + actions, + Edition, + MasterEdition, + NameSymbolTuple, +} from '@oyster/common'; import { MintInfo } from '@solana/spl-token'; import { Connection, PublicKey, PublicKeyAndAccount } from '@solana/web3.js'; import BN from 'bn.js'; import React, { useContext, useEffect, useState } from 'react'; +const { MetadataKey } = actions; export interface MetaContextState { metadata: ParsedAccount[]; + nameSymbolTuples: Record>; + editions: Record>; + masterEditions: Record>; } -const MetaContext = React.createContext( - { - metadata: [] - }, -); +const MetaContext = React.createContext({ + metadata: [], + nameSymbolTuples: {}, + masterEditions: {}, + editions: {}, +}); export function MetaProvider({ children = null as any }) { const connection = useConnection(); const [metadata, setMetadata] = useState[]>([]); + const [nameSymbolTuples, setNameSymbolTuples] = useState< + Record> + >({}); + const [masterEditions, setMasterEditions] = useState< + Record> + >({}); + const [editions, setEditions] = useState< + Record> + >({}); useEffect(() => { let dispose = () => {}; (async () => { - const mintToMetadata = new Map>(); - const processMetaData = (meta: PublicKeyAndAccount) => { - try{ - const metadata = decodeMetadata(meta.account.data); - if(isValidHttpUrl(metadata.uri) && metadata.uri.indexOf('arweave') >= 0) { - const account: ParsedAccount = { + const processMetaData = async (meta: PublicKeyAndAccount) => { + try { + if (meta.account.data[0] == MetadataKey.MetadataV1) { + const metadata = await decodeMetadata(meta.account.data); + if ( + isValidHttpUrl(metadata.uri) && + metadata.uri.indexOf('arweave') >= 0 + ) { + const account: ParsedAccount = { + pubkey: meta.pubkey, + account: meta.account, + info: metadata, + }; + mintToMetadata.set(metadata.mint.toBase58(), account); + } + } else if (meta.account.data[0] == MetadataKey.EditionV1) { + const edition = decodeEdition(meta.account.data); + const account: ParsedAccount = { pubkey: meta.pubkey, account: meta.account, - info: metadata, + info: edition, }; - mintToMetadata.set(metadata.mint.toBase58(), account); + setEditions(e => ({ ...e, [meta.pubkey.toBase58()]: account })); + } else if (meta.account.data[0] == MetadataKey.MasterEditionV1) { + const masterEdition = decodeMasterEdition(meta.account.data); + const account: ParsedAccount = { + pubkey: meta.pubkey, + account: meta.account, + info: masterEdition, + }; + setMasterEditions(e => ({ + ...e, + [meta.pubkey.toBase58()]: account, + })); + } else if (meta.account.data[0] == MetadataKey.NameSymbolTupleV1) { + const nameSymbolTuple = decodeNameSymbolTuple(meta.account.data); + const account: ParsedAccount = { + pubkey: meta.pubkey, + account: meta.account, + info: nameSymbolTuple, + }; + setNameSymbolTuples(e => ({ + ...e, + [meta.pubkey.toBase58()]: account, + })); } } catch { // ignore errors @@ -41,22 +106,27 @@ export function MetaProvider({ children = null as any }) { } }; - const accounts = await connection.getProgramAccounts(programIds().metadata); - accounts.forEach(meta => { - processMetaData(meta); - }); + const accounts = await connection.getProgramAccounts( + programIds().metadata, + ); + for (let i = 0; i < accounts.length; i++) { + await processMetaData(accounts[i]); + } await queryExtendedMetadata(connection, setMetadata, mintToMetadata); - let subId = connection.onProgramAccountChange(programIds().metadata, (info) => { + let subId = connection.onProgramAccountChange( + programIds().metadata, + async info => { const id = (info.accountId as unknown) as string; - processMetaData({ + await processMetaData({ pubkey: new PublicKey(id), account: info.accountInfo, }); queryExtendedMetadata(connection, setMetadata, mintToMetadata); - }); + }, + ); dispose = () => { connection.removeProgramAccountChangeListener(subId); }; @@ -64,11 +134,19 @@ export function MetaProvider({ children = null as any }) { return () => { dispose(); - } - }, [connection, setMetadata]) + }; + }, [ + connection, + setMetadata, + setMasterEditions, + setNameSymbolTuples, + setEditions, + ]); return ( - + {children} ); @@ -77,39 +155,55 @@ export function MetaProvider({ children = null as any }) { const queryExtendedMetadata = async ( connection: Connection, setMetadata: (metadata: ParsedAccount[]) => void, - mintToMeta: Map>) => { - + mintToMeta: Map>, +) => { const mintToMetadata = new Map>(mintToMeta); const extendedMetadataFetch = new Map>(); - const mints = await getMultipleAccounts(connection, [...mintToMetadata.keys()].filter(k => !cache.get(k)), 'single'); + const mints = await getMultipleAccounts( + connection, + [...mintToMetadata.keys()].filter(k => !cache.get(k)), + 'single', + ); mints.keys.forEach((key, index) => { const mintAccount = mints.array[index]; - const mint = cache.add(key, mintAccount, MintParser) as ParsedAccount; - if(mint.info.supply.gt(new BN(1)) || mint.info.decimals !== 0) { + const mint = cache.add( + key, + mintAccount, + MintParser, + ) as ParsedAccount; + if (mint.info.supply.gt(new BN(1)) || mint.info.decimals !== 0) { // naive not NFT check mintToMetadata.delete(key); } else { const metadata = mintToMetadata.get(key); - if(metadata && metadata.info.uri) { - extendedMetadataFetch.set(key, fetch(metadata.info.uri).then(async _ => { - try { - metadata.info.extended = await _.json(); - if (!metadata.info.extended || metadata.info.extended?.files?.length === 0) { - mintToMetadata.delete(key); - } else { - if(metadata.info.extended?.image) { - metadata.info.extended.image = `${metadata.info.uri}/${metadata.info.extended.image}`; + if (metadata && metadata.info.uri) { + extendedMetadataFetch.set( + key, + fetch(metadata.info.uri) + .then(async _ => { + try { + metadata.info.extended = await _.json(); + if ( + !metadata.info.extended || + metadata.info.extended?.files?.length === 0 + ) { + mintToMetadata.delete(key); + } else { + if (metadata.info.extended?.image) { + metadata.info.extended.image = `${metadata.info.uri}/${metadata.info.extended.image}`; + } + } + } catch { + mintToMetadata.delete(key); + return undefined; } - } - } catch { - mintToMetadata.delete(key); - return undefined; - } - }).catch(() => { - mintToMetadata.delete(key); - return undefined; - })); + }) + .catch(() => { + mintToMetadata.delete(key); + return undefined; + }), + ); } } }); @@ -133,5 +227,5 @@ function isValidHttpUrl(text: string) { return false; } - return url.protocol === "http:" || url.protocol === "https:"; + return url.protocol === 'http:' || url.protocol === 'https:'; } diff --git a/packages/metavinci/src/hooks/useArt.tsx b/packages/metavinci/src/hooks/useArt.tsx index 2b92e71..cdcf522 100644 --- a/packages/metavinci/src/hooks/useArt.tsx +++ b/packages/metavinci/src/hooks/useArt.tsx @@ -5,9 +5,13 @@ import { Art } from '../types'; export const useArt = (id: PublicKey | string) => { const { metadata } = useMeta(); + console.log(metadata); - const key = typeof id === 'string' ? id : (id?.toBase58() || ''); - const account = useMemo(() => metadata.find(a => a.pubkey.toBase58() === key), [key, metadata]); + const key = typeof id === 'string' ? id : id?.toBase58() || ''; + const account = useMemo( + () => metadata.find(a => a.pubkey.toBase58() === key), + [key, metadata], + ); return { image: account?.info.extended?.image, @@ -16,4 +20,4 @@ export const useArt = (id: PublicKey | string) => { about: account?.info.extended?.description, royalties: account?.info.extended?.royalty, } as Art; -} +}; diff --git a/packages/metavinci/src/hooks/useUserArts.tsx b/packages/metavinci/src/hooks/useUserArts.tsx index c23b0f8..cfff151 100644 --- a/packages/metavinci/src/hooks/useUserArts.tsx +++ b/packages/metavinci/src/hooks/useUserArts.tsx @@ -1,16 +1,50 @@ import { TokenAccount, useUserAccounts } from '@oyster/common'; -import React, { useMemo } from 'react'; +import { SafetyDepositDraft } from '../actions/createAuctionManager'; import { useMeta } from './../contexts'; -export const useUserArts = () => { - const { metadata } = useMeta(); +export const useUserArts = (): SafetyDepositDraft[] => { + const { metadata, masterEditions, editions, nameSymbolTuples } = useMeta(); const { userAccounts } = useUserAccounts(); const accountByMint = userAccounts.reduce((prev, acc) => { prev.set(acc.info.mint.toBase58(), acc); return prev; }, new Map()); - const ownedMetadata = metadata.filter(m => accountByMint.has(m.info.mint.toBase58())); + const ownedMetadata = metadata.filter(m => + accountByMint.has(m.info.mint.toBase58()), + ); - return ownedMetadata; -} + const possibleNameSymbols = ownedMetadata.map(m => + m.info.nameSymbolTuple + ? nameSymbolTuples[m.info.nameSymbolTuple?.toBase58()] + : undefined, + ); + + const possibleEditions = ownedMetadata.map(m => + m.info.edition ? editions[m.info.edition?.toBase58()] : undefined, + ); + + const possibleMasterEditions = ownedMetadata.map(m => + m.info.masterEdition + ? masterEditions[m.info.masterEdition?.toBase58()] + : undefined, + ); + + let safetyDeposits: SafetyDepositDraft[] = []; + let i = 0; + ownedMetadata.forEach(m => { + let a = accountByMint.get(m.info.mint.toBase58()); + if (a) { + safetyDeposits.push({ + holding: a.pubkey, + nameSymbol: possibleNameSymbols[i], + edition: possibleEditions[i], + masterEdition: possibleMasterEditions[i], + metadata: m, + }); + } + i++; + }); + + return safetyDeposits; +}; diff --git a/packages/metavinci/src/models/metaplex/index.ts b/packages/metavinci/src/models/metaplex/index.ts index 90a1a53..9b49821 100644 --- a/packages/metavinci/src/models/metaplex/index.ts +++ b/packages/metavinci/src/models/metaplex/index.ts @@ -403,19 +403,3 @@ export async function getMetadata(tokenMint: PublicKey): Promise { ) )[0]; } - -export async function getEdition(tokenMint: PublicKey): Promise { - const PROGRAM_IDS = programIds(); - - return ( - await PublicKey.findProgramAddress( - [ - Buffer.from(METADATA_PREFIX), - PROGRAM_IDS.metadata.toBuffer(), - tokenMint.toBuffer(), - Buffer.from(EDITION), - ], - PROGRAM_IDS.metadata, - ) - )[0]; -} diff --git a/packages/metavinci/src/models/metaplex/validateSafetyDepositBox.ts b/packages/metavinci/src/models/metaplex/validateSafetyDepositBox.ts index fa6aa26..1af2e29 100644 --- a/packages/metavinci/src/models/metaplex/validateSafetyDepositBox.ts +++ b/packages/metavinci/src/models/metaplex/validateSafetyDepositBox.ts @@ -3,6 +3,7 @@ import { VAULT_SCHEMA, METADATA_PREFIX, EDITION, + getEdition, } from '@oyster/common'; import { PublicKey, @@ -14,7 +15,6 @@ import { serialize } from 'borsh'; import { getAuctionKeys, - getEdition, getOriginalAuthority, METAPLEX_PREFIX, ValidateSafetyDepositBoxArgs, @@ -23,7 +23,7 @@ import { export async function validateSafetyDepositBox( vault: PublicKey, metadata: PublicKey, - nameSymbol: PublicKey, + nameSymbol: PublicKey | undefined, safetyDepositBox: PublicKey, store: PublicKey, tokenMint: PublicKey, @@ -59,7 +59,7 @@ export async function validateSafetyDepositBox( isWritable: true, }, { - pubkey: nameSymbol, + pubkey: nameSymbol || SystemProgram.programId, isSigner: false, isWritable: true, }, diff --git a/packages/metavinci/src/views/auctionCreate/index.tsx b/packages/metavinci/src/views/auctionCreate/index.tsx index f414a0a..aa81dc2 100644 --- a/packages/metavinci/src/views/auctionCreate/index.tsx +++ b/packages/metavinci/src/views/auctionCreate/index.tsx @@ -48,6 +48,7 @@ import { SCHEMA, } from '../../models/metaplex'; import { serialize } from 'borsh'; +import { SafetyDepositDraft } from '../../actions/createAuctionManager'; const { Step } = Steps; const { Option } = Select; @@ -73,7 +74,7 @@ export interface AuctionState { reservationPrice: number; // listed NFTs - items: ParsedAccount[]; + items: SafetyDepositDraft[]; // number of editions for this auction (only applicable to limited edition) editions?: number;