-
-
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
Can you explain how the first parameter of
findProgramAddress
[ Buffer.from("metadata"), TOKEN_METADATA_PROGRAM_ID.toBuffer(), tokenMint.toBuffer(), ]
works?