Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor metadata provider to be single-mint only #85

Merged
merged 1 commit into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 90 additions & 58 deletions sdk/src/metadataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {
} from '@magiceden-oss/open_creator_protocol';
import { Metaplex } from '@metaplex-foundation/js';
import { Creator, Metadata, TokenStandard } from 'old-mpl-token-metadata';
import { Connection, PublicKey } from '@solana/web3.js';
import { AccountInfo, Connection, PublicKey } from '@solana/web3.js';
import { Mint, unpackMint } from '@solana/spl-token';

export class MetadataProviderError extends Error {
name = 'MetadataProviderError';
Expand All @@ -18,87 +19,118 @@ export interface MintStateWithAddress {
mintStateAddress: PublicKey;
}

export abstract class MetadataProvider {
abstract load(mint: PublicKey): Promise<void>;
abstract getCreators(mint: PublicKey): Creator[];
abstract getTokenStandard(mint: PublicKey): TokenStandard | undefined;
abstract getRuleset(mint: PublicKey): PublicKey | undefined;
abstract getMintState(mint: PublicKey): MintStateWithAddress | undefined;
abstract getLoadedMint(): PublicKey | undefined;
/**
* Interface for providing metadata for a given mint
*/
export interface MetadataProvider {
get creators(): Creator[];
get tokenStandard(): TokenStandard | undefined;
get ruleset(): PublicKey | undefined;
get mintState(): MintStateWithAddress | undefined;
get mintAddress(): PublicKey;
get tokenProgram(): PublicKey;
get sellerFeeBasisPoints(): number;
}

checkMetadataMint(mint: PublicKey) {
const loadedMint = this.getLoadedMint();
if (!loadedMint) {
throw new MetadataProviderError('no metadata loaded');
export class RpcMetadataProvider implements MetadataProvider {
constructor(
private readonly mint: PublicKey,
public readonly metadataAccount: Metadata,
public readonly mintAccount: Mint & { tokenProgramId: PublicKey },
public readonly mintStateAccount: MintStateWithAddress | undefined,
) {
if (!mint.equals(metadataAccount.mint)) {
throw new MetadataProviderError('mint and metadata mismatch');
}
if (!loadedMint.equals(mint)) {
throw new MetadataProviderError('mint mismatch');
}

static async loadFromRpc(
mint: PublicKey,
connection: Connection,
): Promise<RpcMetadataProvider> {
const mpl = new Metaplex(connection);
const metadataAddress = mpl.nfts().pdas().metadata({ mint });
const mintStateAddress = findMintStatePk(mint);
const [metadataAi, mintStateAi, mintAi] =
await connection.getMultipleAccountsInfo([
metadataAddress,
mintStateAddress,
mint,
]);

if (!metadataAi) {
throw new MetadataProviderError('metadata not found');
}
if (!mintAi) {
throw new MetadataProviderError('mint not found');
}
return RpcMetadataProvider.loadFromAccountInfos(mint, mintStateAddress, {
metadata: metadataAi,
mint: mintAi,
mintState: mintStateAi,
});
}
}

export class RpcMetadataProvider extends MetadataProvider {
private connection: Connection;
private mpl: Metaplex;
mintState: MintStateWithAddress | undefined;
metadata: Metadata | undefined;
static loadFromAccountInfos(
mint: PublicKey,
mintStateAddress: PublicKey,
accounts: {
mint: AccountInfo<Buffer>;
metadata: AccountInfo<Buffer>;
mintState: AccountInfo<Buffer> | null;
},
): RpcMetadataProvider {
return new RpcMetadataProvider(
mint,
Metadata.fromAccountInfo(accounts.metadata)[0],
{
...unpackMint(mint, accounts.mint, accounts.mint.owner),
tokenProgramId: accounts.mint.owner,
},
parseMintState(mintStateAddress, accounts.mint),
);
}

constructor(conn: Connection) {
super();
this.connection = conn;
this.mpl = new Metaplex(conn);
get creators(): Creator[] {
return this.metadataAccount.data.creators ?? [];
}

async load(mint: PublicKey) {
const metadataAddress = this.mpl.nfts().pdas().metadata({ mint });
[this.metadata, this.mintState] = await Promise.all([
Metadata.fromAccountAddress(this.connection, metadataAddress),
getMintState(this.connection, mint),
]);
get tokenStandard(): TokenStandard | undefined {
return this.metadataAccount.tokenStandard ?? undefined;
}

getCreators(mint: PublicKey): Creator[] {
this.checkMetadataMint(mint);
return this.metadata!.data.creators ?? [];
get ruleset(): PublicKey | undefined {
return this.metadataAccount.programmableConfig?.ruleSet ?? undefined;
}

getTokenStandard(mint: PublicKey): TokenStandard | undefined {
this.checkMetadataMint(mint);
return this.metadata!.tokenStandard ?? undefined;
get mintState(): MintStateWithAddress | undefined {
return this.mintStateAccount;
}

getRuleset(mint: PublicKey): PublicKey | undefined {
this.checkMetadataMint(mint);
return this.metadata!.programmableConfig?.ruleSet ?? undefined;
get mintAddress(): PublicKey {
return this.mint;
}

getMintState(mint: PublicKey): MintStateWithAddress | undefined {
// check metadata as a proxy check to make sure mint was loaded corrrectly
this.checkMetadataMint(mint);
return this.mintState;
get tokenProgram(): PublicKey {
return this.mintAccount.tokenProgramId;
}

getLoadedMint(): PublicKey | undefined {
return this.metadata?.mint;
get sellerFeeBasisPoints(): number {
return this.metadataAccount.data.sellerFeeBasisPoints;
}
}

async function getMintState(
connection: Connection,
tokenMint: PublicKey,
): Promise<MintStateWithAddress | undefined> {
const mintStateId = findMintStatePk(tokenMint);
function parseMintState(
mintStateId: PublicKey,
mintStateAccountInfo: AccountInfo<Buffer> | null,
): MintStateWithAddress | undefined {
if (!mintStateAccountInfo) {
return undefined;
}
try {
const mintState = await MintState.fromAccountAddress(
connection,
mintStateId,
);
const mintState = MintState.fromAccountInfo(mintStateAccountInfo)[0];
return { mintStateAddress: mintStateId, mintState };
} catch (_e) {
return undefined;
}
}

export function rpcMetadataProviderGenerator(connection: Connection) {
return new RpcMetadataProvider(connection);
}
Loading
Loading