Skip to content

Commit

Permalink
refactor metadata provider to be single-mint only (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
madergaser authored Feb 21, 2024
1 parent f4a4fa5 commit cac7c65
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 109 deletions.
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

0 comments on commit cac7c65

Please sign in to comment.