Skip to content

Instantly share code, notes, and snippets.

@dvcrn
Last active June 15, 2024 23:34
Show Gist options
  • Save dvcrn/c099c9b5a095ffe4ddb6481c22cde5f4 to your computer and use it in GitHub Desktop.
Save dvcrn/c099c9b5a095ffe4ddb6481c22cde5f4 to your computer and use it in GitHub Desktop.
get metadata from metaplex
// 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;
};
@tevdoradze
Copy link

tevdoradze commented Sep 11, 2021

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

@dvcrn
Copy link
Author

dvcrn commented Sep 13, 2021

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

@GrimLothar
Copy link

Do you have any idea why I might be getting this error? it seems to be inside the borsh library which i have imported in that file already...
Screen Shot 2021-09-17 at 11 55 13 AM

@dvcrn
Copy link
Author

dvcrn commented Sep 22, 2021

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

@ivan-homoliak-sutd
Copy link

ivan-homoliak-sutd commented Sep 29, 2021

Deserialization of Metadata does not work for me on the input token address ABJQmmSjJ3jpt4EKYbbkuFjMNNPpEVrKoDckHqqKnmPD:

image

My ts-node version: v10.2.1

Do you see the problem?

@KiranMorrison53
Copy link

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

@KartikSoneji
Copy link

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?

@dvcrn
Copy link
Author

dvcrn commented Nov 20, 2021

It's now much easier to get and decode metadata with the metaplex js package: https://gist.github.com/dvcrn/a1b0ff0a0b4b3ab02aff44bc84ac4522

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment