-
-
Save dvcrn/c099c9b5a095ffe4ddb6481c22cde5f4 to your computer and use it in GitHub Desktop.
| // EDIT: It's now much easier to get metadata from metaplex using the js package. | |
| // Check the new gist here: https://gist.github.com/dvcrn/a1b0ff0a0b4b3ab02aff44bc84ac4522 | |
| // I didn't want to edit this gist because a lot of people found it helpful to see how to manually decode a account | |
| import * as web3 from "@solana/web3.js"; | |
| import * as metadata from "./metadata"; // see metadata.ts | |
| const tokenAddress = new web3.PublicKey( | |
| "CxkKDaBvtHqg8aHBVY8E4YYBsCfJkJVsTAEdTo5k4SEw" | |
| ); | |
| (async () => { | |
| // Connect to cluster | |
| var connection = new web3.Connection( | |
| web3.clusterApiUrl("mainnet-beta"), | |
| "confirmed" | |
| ); | |
| // get metadata account that holds the metadata information | |
| const m = await metadata.getMetadataAccount(tokenAddress); | |
| console.log("metadata acc: ", m); | |
| // get the account info for that account | |
| const accInfo = await connection.getAccountInfo(m); | |
| console.log(accInfo); | |
| // finally, decode metadata | |
| console.log(metadata.decodeMetadata(accInfo!.data)); | |
| })(); |
| /** | |
| * This blob of a file is pulled together from different files from the metaplex | |
| * repository. | |
| * Metaplex does not have a NPM package at the current time to make this easier, so instead of | |
| * trying to reference their stuff, I copied all of the minimum necessary code into this file | |
| */ | |
| import { BinaryReader, BinaryWriter, deserializeUnchecked } from "borsh"; | |
| import { PublicKey } from "@solana/web3.js"; | |
| import base58 from "bs58"; | |
| export const METADATA_PROGRAM_ID = | |
| "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" as StringPublicKey; | |
| export const METADATA_PREFIX = "metadata"; | |
| const PubKeysInternedMap = new Map<string, PublicKey>(); | |
| // Borsh extension for pubkey stuff | |
| (BinaryReader.prototype as any).readPubkey = function () { | |
| const reader = this as unknown as BinaryReader; | |
| const array = reader.readFixedArray(32); | |
| return new PublicKey(array); | |
| }; | |
| (BinaryWriter.prototype as any).writePubkey = function (value: PublicKey) { | |
| const writer = this as unknown as BinaryWriter; | |
| writer.writeFixedArray(value.toBuffer()); | |
| }; | |
| (BinaryReader.prototype as any).readPubkeyAsString = function () { | |
| const reader = this as unknown as BinaryReader; | |
| const array = reader.readFixedArray(32); | |
| return base58.encode(array) as StringPublicKey; | |
| }; | |
| (BinaryWriter.prototype as any).writePubkeyAsString = function ( | |
| value: StringPublicKey | |
| ) { | |
| const writer = this as unknown as BinaryWriter; | |
| writer.writeFixedArray(base58.decode(value)); | |
| }; | |
| const toPublicKey = (key: string | PublicKey) => { | |
| if (typeof key !== "string") { | |
| return key; | |
| } | |
| let result = PubKeysInternedMap.get(key); | |
| if (!result) { | |
| result = new PublicKey(key); | |
| PubKeysInternedMap.set(key, result); | |
| } | |
| return result; | |
| }; | |
| const findProgramAddress = async ( | |
| seeds: (Buffer | Uint8Array)[], | |
| programId: PublicKey | |
| ) => { | |
| const key = | |
| "pda-" + | |
| seeds.reduce((agg, item) => agg + item.toString("hex"), "") + | |
| programId.toString(); | |
| const result = await PublicKey.findProgramAddress(seeds, programId); | |
| return [result[0].toBase58(), result[1]] as [string, number]; | |
| }; | |
| export type StringPublicKey = string; | |
| export enum MetadataKey { | |
| Uninitialized = 0, | |
| MetadataV1 = 4, | |
| EditionV1 = 1, | |
| MasterEditionV1 = 2, | |
| MasterEditionV2 = 6, | |
| EditionMarker = 7, | |
| } | |
| class Creator { | |
| address: StringPublicKey; | |
| verified: boolean; | |
| share: number; | |
| constructor(args: { | |
| address: StringPublicKey; | |
| verified: boolean; | |
| share: number; | |
| }) { | |
| this.address = args.address; | |
| this.verified = args.verified; | |
| this.share = args.share; | |
| } | |
| } | |
| class Data { | |
| name: string; | |
| symbol: string; | |
| uri: string; | |
| sellerFeeBasisPoints: number; | |
| creators: Creator[] | null; | |
| constructor(args: { | |
| name: string; | |
| symbol: string; | |
| uri: string; | |
| sellerFeeBasisPoints: number; | |
| creators: Creator[] | null; | |
| }) { | |
| this.name = args.name; | |
| this.symbol = args.symbol; | |
| this.uri = args.uri; | |
| this.sellerFeeBasisPoints = args.sellerFeeBasisPoints; | |
| this.creators = args.creators; | |
| } | |
| } | |
| class Metadata { | |
| key: MetadataKey; | |
| updateAuthority: StringPublicKey; | |
| mint: StringPublicKey; | |
| data: Data; | |
| primarySaleHappened: boolean; | |
| isMutable: boolean; | |
| editionNonce: number | null; | |
| // set lazy | |
| masterEdition?: StringPublicKey; | |
| edition?: StringPublicKey; | |
| constructor(args: { | |
| updateAuthority: StringPublicKey; | |
| mint: StringPublicKey; | |
| data: Data; | |
| primarySaleHappened: boolean; | |
| isMutable: boolean; | |
| editionNonce: number | null; | |
| }) { | |
| this.key = MetadataKey.MetadataV1; | |
| this.updateAuthority = args.updateAuthority; | |
| this.mint = args.mint; | |
| this.data = args.data; | |
| this.primarySaleHappened = args.primarySaleHappened; | |
| this.isMutable = args.isMutable; | |
| this.editionNonce = args.editionNonce; | |
| } | |
| } | |
| const METADATA_SCHEMA = new Map<any, any>([ | |
| [ | |
| Data, | |
| { | |
| kind: "struct", | |
| fields: [ | |
| ["name", "string"], | |
| ["symbol", "string"], | |
| ["uri", "string"], | |
| ["sellerFeeBasisPoints", "u16"], | |
| ["creators", { kind: "option", type: [Creator] }], | |
| ], | |
| }, | |
| ], | |
| [ | |
| Creator, | |
| { | |
| kind: "struct", | |
| fields: [ | |
| ["address", "pubkeyAsString"], | |
| ["verified", "u8"], | |
| ["share", "u8"], | |
| ], | |
| }, | |
| ], | |
| [ | |
| Metadata, | |
| { | |
| kind: "struct", | |
| fields: [ | |
| ["key", "u8"], | |
| ["updateAuthority", "pubkeyAsString"], | |
| ["mint", "pubkeyAsString"], | |
| ["data", Data], | |
| ["primarySaleHappened", "u8"], // bool | |
| ["isMutable", "u8"], // bool | |
| ], | |
| }, | |
| ], | |
| ]); | |
| export async function getMetadataAccount( | |
| tokenMint: StringPublicKey | |
| ): Promise<StringPublicKey> { | |
| return ( | |
| await findProgramAddress( | |
| [ | |
| Buffer.from(METADATA_PREFIX), | |
| toPublicKey(METADATA_PROGRAM_ID).toBuffer(), | |
| toPublicKey(tokenMint).toBuffer(), | |
| ], | |
| toPublicKey(METADATA_PROGRAM_ID) | |
| ) | |
| )[0]; | |
| } | |
| const METADATA_REPLACE = new RegExp("\u0000", "g"); | |
| export const decodeMetadata = (buffer: Buffer): Metadata => { | |
| const metadata = deserializeUnchecked( | |
| METADATA_SCHEMA, | |
| Metadata, | |
| buffer | |
| ) as Metadata; | |
| metadata.data.name = metadata.data.name.replace(METADATA_REPLACE, ""); | |
| metadata.data.uri = metadata.data.uri.replace(METADATA_REPLACE, ""); | |
| metadata.data.symbol = metadata.data.symbol.replace(METADATA_REPLACE, ""); | |
| return metadata; | |
| }; |
Can you explain how the first parameter of
findProgramAddress[ Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), tokenMint.toBuffer(), ]works?
It's searching a "program derived address" - https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses
Basically, an address that doesn't have a private key (so the user can't sign), but the program can use it to sign when communicating with other programs.
Metaplex documentation specifies this exact format, a PDA that's derived from:
- the string "metadata"
- the token program ID
- the public key of the token mint
I've updated this gist with the full metadata-related decoding stuff from the metaplex repository. This should now all you need to display metadata from metaplex
running your code as is results in the error:
`➜ solanaTS npx ts-node index.ts
⨯ Unable to compile TypeScript:
index.ts:16:47 - error TS2345: Argument of type 'PublicKey' is not assignable to parameter of type 'string'.
16 const m = await metadata.getMetadataAccount(tokenAddress);
~~~~~~~~~~~~
index.ts:20:51 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'PublicKey'.
20 const accInfo = await connection.getAccountInfo(m);`
any idea what's going on?
thanks
const findProgramAddress = async (
seeds: (Buffer | Uint8Array)[],
programId: PublicKey
) => {
const key =
"pda-" +
seeds.reduce((agg, item) => agg + item.toString("hex"), "") +
programId.toString();
const result = await PublicKey.findProgramAddress(seeds, programId);
return [result[0].toBase58(), result[1]] as [string, number];
};https://gist.github.com/dvcrn/c099c9b5a095ffe4ddb6481c22cde5f4#file-metadata-ts-L60-L63
Here, key doesn't seem to be used, is it needed?
It's now much easier to get and decode metadata with the metaplex js package: https://gist.github.com/dvcrn/a1b0ff0a0b4b3ab02aff44bc84ac4522
It's now much easier to get and decode metadata with the metaplex js package: https://gist.github.com/dvcrn/a1b0ff0a0b4b3ab02aff44bc84ac4522
mplx packages sucks and all kinds of version interop problems as of now
it's an overengineered piece of bloatware if i seen any


Can you explain how the first parameter of
findProgramAddress[ Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), tokenMint.toBuffer(), ]works?