Massive WIP on useArt, adding safety deposit draft eveyrwhere

This commit is contained in:
Jordan Prince 2021-04-25 21:38:22 -05:00
parent 03a4cab50b
commit 3807f564c3
8 changed files with 295 additions and 85 deletions

View File

@ -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<any, any>([
],
},
],
[
Edition,
{
kind: 'struct',
fields: [
['key', 'u8'],
['parent', 'pubkey'],
['edition', 'u64'],
],
},
],
[
Metadata,
{
@ -246,8 +276,36 @@ export const METADATA_SCHEMA = new Map<any, any>([
],
]);
export const decodeMetadata = (buffer: Buffer) => {
return deserializeBorsh(METADATA_SCHEMA, Metadata, buffer) as Metadata;
export const decodeMetadata = async (buffer: Buffer): Promise<Metadata> => {
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<PublicKey> {
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<PublicKey> {
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];
}

View File

@ -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<Metadata>;
nameSymbol: ParsedAccount<NameSymbolTuple>;
masterEdition: ParsedAccount<MasterEdition>;
nameSymbol?: ParsedAccount<NameSymbolTuple>;
masterEdition?: ParsedAccount<MasterEdition>;
edition?: ParsedAccount<Edition>;
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,

View File

@ -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<Metadata>[];
nameSymbolTuples: Record<string, ParsedAccount<NameSymbolTuple>>;
editions: Record<string, ParsedAccount<Edition>>;
masterEditions: Record<string, ParsedAccount<MasterEdition>>;
}
const MetaContext = React.createContext<MetaContextState>(
{
metadata: []
},
);
const MetaContext = React.createContext<MetaContextState>({
metadata: [],
nameSymbolTuples: {},
masterEditions: {},
editions: {},
});
export function MetaProvider({ children = null as any }) {
const connection = useConnection();
const [metadata, setMetadata] = useState<ParsedAccount<Metadata>[]>([]);
const [nameSymbolTuples, setNameSymbolTuples] = useState<
Record<string, ParsedAccount<NameSymbolTuple>>
>({});
const [masterEditions, setMasterEditions] = useState<
Record<string, ParsedAccount<MasterEdition>>
>({});
const [editions, setEditions] = useState<
Record<string, ParsedAccount<Edition>>
>({});
useEffect(() => {
let dispose = () => {};
(async () => {
const mintToMetadata = new Map<string, ParsedAccount<Metadata>>();
const processMetaData = (meta: PublicKeyAndAccount<Buffer>) => {
try{
const metadata = decodeMetadata(meta.account.data);
if(isValidHttpUrl(metadata.uri) && metadata.uri.indexOf('arweave') >= 0) {
const account: ParsedAccount<Metadata> = {
const processMetaData = async (meta: PublicKeyAndAccount<Buffer>) => {
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<Metadata> = {
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<Edition> = {
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<MasterEdition> = {
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<NameSymbolTuple> = {
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 (
<MetaContext.Provider value={{ metadata }}>
<MetaContext.Provider
value={{ metadata, editions, masterEditions, nameSymbolTuples }}
>
{children}
</MetaContext.Provider>
);
@ -77,39 +155,55 @@ export function MetaProvider({ children = null as any }) {
const queryExtendedMetadata = async (
connection: Connection,
setMetadata: (metadata: ParsedAccount<Metadata>[]) => void,
mintToMeta: Map<string, ParsedAccount<Metadata>>) => {
mintToMeta: Map<string, ParsedAccount<Metadata>>,
) => {
const mintToMetadata = new Map<string, ParsedAccount<Metadata>>(mintToMeta);
const extendedMetadataFetch = new Map<string, Promise<any>>();
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<MintInfo>;
if(mint.info.supply.gt(new BN(1)) || mint.info.decimals !== 0) {
const mint = cache.add(
key,
mintAccount,
MintParser,
) as ParsedAccount<MintInfo>;
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:';
}

View File

@ -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;
}
};

View File

@ -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<string, TokenAccount>());
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;
};

View File

@ -403,19 +403,3 @@ export async function getMetadata(tokenMint: PublicKey): Promise<PublicKey> {
)
)[0];
}
export async function getEdition(tokenMint: PublicKey): Promise<PublicKey> {
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];
}

View File

@ -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,
},

View File

@ -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<Metadata>[];
items: SafetyDepositDraft[];
// number of editions for this auction (only applicable to limited edition)
editions?: number;