Summary #
- The
metadata pointer
extension associates a token mint directly to a metadata account. This happens by storing the metadata account's address in the mint. This metadata account address can be an external metadata account, like Metaplex, or can be the mint itself if using themetadata
extension. - The
metadata
mint extension allows embedding of metadata directly into mint accounts through the Token Extensions Program. This is always accompanied with a self-referencingmetadata pointer
. This facilitates embedding comprehensive token information at the minting stage. - These extensions enhance the interoperability of tokens across different applications and platforms by standardizing how metadata is associated and accessed.
- Directly embedding or pointing to metadata can streamline transactions and interactions by reducing the need for additional lookups or external calls.
Overview #
The Token Extensions Program streamlines metadata on Solana. Without the Token
Extensions Program, developers store metadata in metadata accounts using a
metadata onchain program; mainly Metaplex
. However, this has some drawbacks.
For example the mint account to which the metadata is "attached" has no
awareness of the metadata account. To determine if an account has metadata, we
have to PDA the mint and the Metaplex
program together and query the network
to see if a Metadata account exists. Additionally, to create and update this
metadata you have to use a secondary program (i.e. Metaplex
). These processes
introduces vender lock in and increased complexity. Token Extension Programs's
Metadata extensions fix this by introducing two extensions:
metadata-pointer
extension: Adds two simple fields in the mint account itself: a publicKey pointer to the account that holds the metadata for the token following the Token-Metadata Interface, and the authority to update this pointer.metadata
extension: Adds the fields described in the Token-Metadata Interface which allows us to store the metadata in the mint itself.
The metadata
extension must be used in conjunction with
the metadata-pointer
extension which points back to the mint itself.
Metadata-Pointer extension: #
Since multiple metadata programs exist, a mint can have numerous accounts
claiming to describe the mint, making it complicated to know which one is the
mint's "official" metadata. To resolve this, the metadata-pointer
extension
adds a publicKey
field to the mint account called metadataAddress
, which
points to the account that holds the metadata for this token. To avoid imitation
mints claiming to be a stablecoin, a client can now check whether the mint and
the metadata point to each other.
The extension adds two new fields to the mint account to accomplish this:
metadataAddress
: Holds the metadata account address for this token; it can point to itself if you use themetadata
extension.authority
: The authority that can set the metadata address.
The extension also introduces three new helper functions:
createInitializeMetadataPointerInstruction
createUpdateMetadataPointerInstruction
getMetadataPointerState
The function createInitializeMetadataPointerInstruction
will return the
instruction that will set the metadata address in the mint account.
This function takes four parameters:
mint
: the mint account that will be createdauthority
: the authority that can set the metadata addressmetadataAddress
: the account address that holds the metadataprogramId
: the SPL Token program ID (in this case, it will be the Token Extension program ID)
function createInitializeMetadataPointerInstruction(
mint: PublicKey,
authority: PublicKey | null,
metadataAddress: PublicKey | null,
programId: PublicKey,
);
The createUpdateMetadataPointerInstruction
function returns an instruction
that will update the mint account's metadata address. You can update the
metadata pointer at any point if you hold the authority.
This function takes five parameters:
mint
: the mint account that will be created.authority
: the authority that can set the metadata addressmetadataAddress
: the account address that holds the metadatamultiSigners
: the multi-signers that will sign the transactionprogramId
: the SPL Token program ID (in this case, it will be the Token Extension program ID)
function createUpdateMetadataPointerInstruction(
mint: PublicKey,
authority: PublicKey,
metadataAddress: PublicKey | null,
multiSigners: (Signer | PublicKey)[] = [],
programId: PublicKey = TOKEN_2022_PROGRAM_ID,
);
The getMetadataPointerState
function will return the MetadataPointer
state
for the given Mint
object. We can get this using the getMint
function.
function getMetadataPointerState(mint: Mint): Partial<MetadataPointer> | null;
export interface MetadataPointer {
/** Optional authority that can set the metadata address */
authority: PublicKey | null;
/** Optional Account Address that holds the metadata */
metadataAddress: PublicKey | null;
}
Create NFT with metadata-pointer #
To create an NFT with the metadata-pointer
extension, we need two new
accounts: the mint
and the metadataAccount
.
The mint
is usually a new Keypair
created by Keypair.generate()
. The
metadataAccount
can be the mint
's publicKey
if using the metadata mint
extension or another metadata account like from Metaplex.
At this point, the mint
is only a Keypair
, but we need to save space for it
on the blockchain. All accounts on the Solana blockchain owe rent proportional
to the size of the account, and we need to know how big the mint account is in
bytes. We can use the getMintLen
method from the @solana/spl-token
library.
The metadata-pointer extension increases the size of the mint account by adding
two new fields: metadataAddress
and authority
.
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
To create and initialize the mint
with the metadata pointer, we need several
instructions in a particular order:
- Create the
mint
account, which reserves space on the blockchain withSystemProgram.createAccount
- Initialize the metadata pointer extension with
createInitializeMetadataPointerInstruction
- Initialize the mint itself with
createInitializeMintInstruction
const createMintAccountInstructions = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
lamports,
newAccountPubkey: mint.publicKey,
programId: TOKEN_2022_PROGRAM_ID,
space: mintLen,
});
const initMetadataPointerInstructions =
createInitializeMetadataPointerInstruction(
mint.publicKey,
payer.publicKey,
metadataAccount,
TOKEN_2022_PROGRAM_ID,
);
const initMintInstructions = createInitializeMintInstruction(
mint.publicKey,
decimals,
payer.publicKey,
payer.publicKey,
TOKEN_2022_PROGRAM_ID,
);
To create the NFT, add the instructions to a transaction and send it to the Solana network:
const transaction = new Transaction().add(
createMintAccountInstructions,
initMetadataPointerInstructions,
initMintInstructions,
);
const sig = await sendAndConfirmTransaction(connection, transaction, [
payer,
mint,
]);
Metadata extension: #
The metadata
extension is an exciting addition to the Token Extensions
Program. This extension allows us to store the metadata directly in the mint
itself! This eliminates the need for a separate account, greatly simplifying the
handling of metadata.
The metadata
extension works directly with the
metadata-pointer
extension. During the mint creation, you should also add the
metadata-pointer
extension, pointed at the mint itself. Check out the
Solana Token Extensions Program docs
The added fields and functions in the metadata extension follow the Token-Metadata Interface
When a mint is initialized with the metadata extension, it will store these extra fields:
type Pubkey = [u8; 32];
type OptionalNonZeroPubkey = Pubkey; // if all zeroes, interpreted as `None`
pub struct TokenMetadata {
/// The authority that can sign to update the metadata
pub update_authority: OptionalNonZeroPubkey,
/// The associated mint, used to counter spoofing to be sure that metadata
/// belongs to a particular mint
pub mint: Pubkey,
/// The longer name of the token
pub name: String,
/// The shortened symbol for the token
pub symbol: String,
/// The URI pointing to richer metadata
pub uri: String,
/// Any additional metadata about the token as key-value pairs. The program
/// must avoid storing the same key twice.
pub additional_metadata: Vec<(String, String)>,
}
With these added fields, the @solana/spl-token-metadata
library has been
updated with the following functions to help out:
createInitializeInstruction
createUpdateFieldInstruction
createRemoveKeyInstruction
createUpdateAuthorityInstruction
createEmitInstruction
pack
unpack
Additionally, the @solana/spl-token library introduces a new function and two constants:
getTokenMetadata
LENGTH_SIZE
: a constant number of bytes of the length of the dataTYPE_SIZE
: a constant number of bytes of the type of the data
The function createInitializeInstruction
initializes the metadata in the
account and sets the primary metadata fields (name, symbol, URI). The function
then returns an instruction that will set the metadata fields in the mint
account.
This function takes eight parameters:
mint
: the mint account that will be initializemetadata
: the metadata account that will be createdmintAuthority
: the authority that can mint tokensupdateAuthority
: the authority that can sign to update the metadataname
: the longer name of the tokensymbol
: the shortened symbol for the token, also known as the tickeruri
: the token URI pointing to richer metadataprogramId
: the SPL Token program ID (in this case it will be the Token Extension program ID)
export interface InitializeInstructionArgs {
programId: PublicKey;
metadata: PublicKey;
updateAuthority: PublicKey;
mint: PublicKey;
mintAuthority: PublicKey;
name: string;
symbol: string;
uri: string;
}
export function createInitializeInstruction(
args: InitializeInstructionArgs,
): TransactionInstruction;
The function createUpdateFieldInstruction
returns the instruction that creates
or updates a field in a token-metadata account.
This function takes five parameters:
metadata
: the metadata account address.updateAuthority
: the authority that can sign to update the metadatafield
: the field that we want to update, this is either one of the built inField
s or a custom field stored in theadditional_metadata
fieldvalue
: the updated value of the fieldprogramId
: the SPL Token program Id (in this case it will be the Token Extension program Id)
export enum Field {
Name,
Symbol,
Uri,
}
export interface UpdateFieldInstruction {
programId: PublicKey;
metadata: PublicKey;
updateAuthority: PublicKey;
field: Field | string;
value: string;
}
export function createUpdateFieldInstruction(
args: UpdateFieldInstruction,
): TransactionInstruction;
If the metadata you are updating requires more space than
the initial allocated space, you'll have to pair it with a system.transfer
to
have enough rent for the createUpdateFieldInstruction
to realloc with. You can
get the extra space needed with getAdditionalRentForUpdatedMetadata
. Or if
you're calling this update as a standalone, you can use the
tokenMetadataUpdateFieldWithRentTransfer
helper to do all of this at
once.
The function createRemoveKeyInstruction
returns the instruction that removes
the additional_metadata
field from a token-metadata account.
This function takes five parameters:
metadata
: the metadata account addressupdateAuthority
: the authority that can sign to update the metadatafield
: the field that we want to removeprogramId
: the SPL Token program ID (in this case it will be the Token Extension program ID)idempotent
: When true, instruction will not error if the key does not exist
export interface RemoveKeyInstructionArgs {
programId: PublicKey;
metadata: PublicKey;
updateAuthority: PublicKey;
key: string;
idempotent: boolean;
}
export function createRemoveKeyInstruction(
args: RemoveKeyInstructionArgs,
): TransactionInstruction;
The function createUpdateAuthorityInstruction
returns the instruction that
updates the authority of a token-metadata account.
This function takes four parameters:
metadata
: the metadata account addressoldAuthority
: the current authority that can sign to update the metadatanewAuthority
: the new authority that can sign to update the metadataprogramId
: the SPL Token program ID (in this case it will be the Token Extension program ID)
export interface UpdateAuthorityInstructionArgs {
programId: PublicKey;
metadata: PublicKey;
oldAuthority: PublicKey;
newAuthority: PublicKey | null;
}
export function createUpdateAuthorityInstruction(
args: UpdateAuthorityInstructionArgs,
): TransactionInstruction;
The function createEmitInstruction
"emits" or logs out token-metadata in the
expected TokenMetadata state format. This is a required function for metadata
programs that want to follow the TokenMetadata interface. The emit instruction
allows indexers and other off-chain users to call to get metadata. This also
allows custom metadata programs to store
metadata in a different format while maintaining compatibility with the Interface standards.
This function takes four parameters:
metadata
: the metadata account addressprogramId
: the SPL Token program ID (in this case it will be the Token Extension program ID)start
: Optional the start the metadataend
: Optional the end the metadata
export interface EmitInstructionArgs {
programId: PublicKey;
metadata: PublicKey;
start?: bigint;
end?: bigint;
}
export function createEmitInstruction(
args: EmitInstructionArgs,
): TransactionInstruction;
The pack
function encodes metadata into a byte array, while its counterpart,
unpack
, decodes metadata from a byte array. These operations are essential for
determining the metadata's byte size, crucial for allocating adequate storage
space.
export interface TokenMetadata {
// The authority that can sign to update the metadata
updateAuthority?: PublicKey;
// The associated mint, used to counter spoofing to be sure that metadata belongs to a particular mint
mint: PublicKey;
// The longer name of the token
name: string;
// The shortened symbol for the token
symbol: string;
// The URI pointing to richer metadata
uri: string;
// Any additional metadata about the token as key-value pairs
additionalMetadata: [string, string][];
}
export const pack = (meta: TokenMetadata): Uint8Array
export function unpack(buffer: Buffer | Uint8Array): TokenMetadata
The function getTokenMetadata
returns the metadata for the given mint.
It takes four parameters:
connection
: Connection to useaddress
: mint accountcommitment
: desired level of commitment for querying the stateprogramId
: SPL Token program account (in this case it will be the Token Extension program ID)
export async function getTokenMetadata(
connection: Connection,
address: PublicKey,
commitment?: Commitment,
programId = TOKEN_2022_PROGRAM_ID,
): Promise<TokenMetadata | null>;
Create NFT with metadata extension #
Creating an NFT with the metadata extension is just like creating one with the metadata-pointer with a few extra steps:
- Gather our needed accounts
- Find/decide on the needed size of our metadata
- Create the
mint
account - Initialize the pointer
- Initialize the mint
- Initialize the metadata in the mint account
- Add any additional custom fields if needed
First, the mint
will be a Keypair, usually generated using
Keypair.generate()
. Then, we must decide what metadata to include and
calculate the total size and cost.
A mint account's size with the metadata and metadata-pointer extensions incorporate the following:
- the basic metadata fields: name, symbol, and URI
- the additional custom fields we want to store as a metadata
- the update authority that can change the metadata in the future
- the
LENGTH_SIZE
andTYPE_SIZE
constants from the@solana/spl-token
library - these are sizes associated with mint extensions that are usually added with the callgetMintLen
, but since the metadata extension is variable length, they need to be added manually - the metadata pointer data (this will be the mint's address and is done for consistency)
There is no need to allocate more space than what is
necessary if you're anticipating more metadata. The
createUpdateFieldInstruction
will automatically reallocate space! However,
you'll have to add another system.transfer
transaction to make sure the mint
account has enough rent.
To determine all of this programmatically, we use the getMintLen
and pack
functions from the @solana/spl-token
library:
const metadata: TokenMetadata = {
mint: mint.publicKey,
name: tokenName,
symbol: tokenSymbol,
uri: tokenUri,
additionalMetadata: [["customField", "customValue"]],
};
const mintAndPointerLen = getMintLen([ExtensionType.MetadataPointer]); // Metadata extension is variable length, so we calculate it below
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
const totalLen = mintLen + mintAndPointerLen;
const lamports = await connection.getMinimumBalanceForRentExemption(totalLen);
To actually create and initialize the mint
with the metadata and metadata
pointer, we need several instructions in a particular order:
- Create the
mint
account which reserves space on the blockchain withSystemProgram.createAccount
- Initialize the metadata pointer extension with
createInitializeMetadataPointerInstruction
- Initialize the mint itself with
createInitializeMintInstruction
- Initialize the metadata with
createInitializeInstruction
(this ONLY sets the basic metadata fields) - Optional: Set the custom fields with
createUpdateFieldInstruction
(one field per call)
const createMintAccountInstructions = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
lamports,
newAccountPubkey: mint.publicKey,
programId: TOKEN_2022_PROGRAM_ID,
space: totalLen,
});
const initMetadataPointerInstructions =
createInitializeMetadataPointerInstruction(
mint.publicKey,
payer.publicKey,
mint.publicKey, // we will point to the mint it self as the metadata account
TOKEN_2022_PROGRAM_ID,
);
const initMintInstructions = createInitializeMintInstruction(
mint.publicKey,
decimals,
payer.publicKey,
payer.publicKey,
TOKEN_2022_PROGRAM_ID,
);
const initMetadataInstruction = createInitializeInstruction({
programId: TOKEN_2022_PROGRAM_ID,
mint: mint.publicKey,
metadata: mint.publicKey,
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
mintAuthority: payer.publicKey,
updateAuthority: payer.publicKey,
});
const updateMetadataFieldInstructions = createUpdateFieldInstruction({
metadata: mint.publicKey,
updateAuthority: payer.publicKey,
programId: TOKEN_2022_PROGRAM_ID,
field: metadata.additionalMetadata[0][0],
value: metadata.additionalMetadata[0][1],
});
Wrap all of these instructions in a transaction to create the embedded NFT:
const transaction = new Transaction().add(
createMintAccountInstructions,
initMetadataPointerInstructions,
initMintInstructions,
initMetadataInstruction,
updateMetadataFieldInstructions, // if you want to add any custom field
);
const signature = await sendAndConfirmTransaction(connection, transaction, [
payer,
mint,
]);
Again, the order here matters.
The createUpdateFieldInstruction
updates only one field
at a time. If you want to have more than one custom field, you will have to call
this method multiple times. Additionally, you can use the same method to update
the basic metadata fields as well:
const updateMetadataFieldInstructions = createUpdateFieldInstruction({
metadata: mint.publicKey,
updateAuthority: payer.publicKey,
programId: TOKEN_2022_PROGRAM_ID,
field: "name", // Field | string
value: "new name",
});
Lab #
Now it is time to practice what we have learned so far. In this lab, we'll
create a script that will illustrate how to create an NFT with the metadata
and metadata pointer
extensions.
0. Getting started #
Let's go ahead and clone our starter code:
git clone https://github.com/Unboxed-Software/solana-lab-token22-metadata.git
cd solana-lab-token22-metadata
git checkout starter
npm install
Let's take a look at what's been provided in the starter
branch.
Along with the NodeJS project being initialized with all of the needed
dependencies, two other files have been provided in the src/
directory.
cat.png
helpers.ts
index.ts
cat.png
is the image we'll use for the NFT. Feel free to replace it with
your own image.
we are using Irys on devnet to upload files, this is capped at 100 KiB.
helpers.ts
file provides us with a useful helper function
uploadOffChainMetadata
.
uploadOffChainMetadata
is a helper to store the off-chain metadata on Arweave
using Irys (formerly Bundlr). In this lab we will be more focused on the Token
Extensions Program interaction, so this uploader function is provided. It is
important to note that an NFT or any off-chain metadata can be stored anywhere
with any storage provider like NFT.storage, Solana's
native ShadowDrive, or
Irys (formerly Bundlr). At the end of the day, all you need
is a url to the hosted metadata json file.
This helper has some exported interfaces. These will clean up our functions as we make them.
export interface CreateNFTInputs {
payer: Keypair;
connection: Connection;
tokenName: string;
tokenSymbol: string;
tokenUri: string;
tokenAdditionalMetadata?: Record<string, string>;
}
export interface UploadOffChainMetadataInputs {
tokenName: string;
tokenSymbol: string;
tokenDescription: string;
tokenExternalUrl: string;
tokenAdditionalMetadata?: Record<string, string>;
imagePath: string;
metadataFileName: string;
}
index.ts
is where we'll add our code. Right now, the code sets up a
connection
and initializes a keypair for us to use.
The keypair payer
will be responsible for every payment we need throughout the
whole process. payer
will also hold all the authorities, like the mint
authority, mint freeze authority, etc. While it's possible to use a distinct
keypair for the authorities, for simplicity's sake, we'll continue using
payer
.
Lastly, this lab will all be done on devnet. This is because we are using Irys to upload metadata to Arweave - the requires a devnet or mainnet connection. If you are running into airdropping problems:
- Add the
keypairPath
parameter toinitializeKeypair
- path can be gotten by runningsolana config get
in your terminal - Get the address of your keypair by running
solana address
in your terminal - Copy the address and airdrop some devnet sol from faucet.solana.
1. Uploading the off-chain metadata #
In this section we will decide on our NFT metadata and upload our files to NFT.Storage using the helper functions provided in the starting code.
To upload our off-chain metadata, we need to first prepare an image that will
represent our NFT. We've provided cat.png
, but feel free to replace it with
your own. Most image types are supported by most wallets. (Again devenet Irys
allows up to 100KiB per file)
Next, let's decide on what metadata our NFT will have. The fields we are
deciding on are name
, description
, symbol
, externalUrl
, and some
attributes
(additional metadata). We'll provide some cat adjacent metadata,
but feel free to make up your own.
name
: Cat NFTdescription
= This is a catsymbol
= EMBexternalUrl
= https://solana.com/attributes
={ species: 'Cat' breed: 'Cool' }
Lastly we just need to format all of this data and send it to our helper
function uploadOffChainMetadata
to get the uploaded metadata uri.
When we put all of this together, the index.ts
file will look as follows:
import { Connection } from "@solana/web3.js";
import { initializeKeypair } from "@solana-developers/helpers";
import { uploadOffChainMetadata } from "./helpers";
import dotenv from "dotenv";
dotenv.config();
const connection = new Connection(clusterApiUrl("devnet"), "finalized");
const payer = await initializeKeypair(connection, {
keypairPath: "your/path/to/keypair.json",
});
const imagePath = "src/cat.png";
const metadataPath = "src/temp.json";
const tokenName = "Cat NFT";
const tokenDescription = "This is a cat";
const tokenSymbol = "EMB";
const tokenExternalUrl = "https://solana.com/";
const tokenAdditionalMetadata = {
species: "Cat",
breed: "Cool",
};
const tokenUri = await uploadOffChainMetadata(
{
tokenName,
tokenDescription,
tokenSymbol,
imagePath,
metadataPath,
tokenExternalUrl,
tokenAdditionalMetadata,
},
payer,
);
// You can log the URI here and run the code to test it
console.log("Token URI:", tokenUri);
Now run npm run start
in your terminal and test your code. You should see the
URI logged once the uploading is done. If you visit the link you should see a
JSON object that holds all of our off-chain metadata.
2. Create NFT function #
Creating an NFT involves multiple instructions. As a best practice when writing
scripts that engage with the Solana network, it is best to consolidate all of
these instructions in one transaction due to the atomic nature of transactions.
This ensures either the successful execution of all instructions or a complete
rollback in case of errors. That being said, we're going to make a new function
createNFTWithEmbeddedMetadata
in a new file called
src/nft-with-embedded-metadata.ts
.
This function will create an NFT by doing the following:
- Create the metadata object
- Allocate the mint
- Initialize the metadata-pointer making sure that it points to the mint itself
- Initialize the mint
- Initialize the metadata inside the mint (that will set name, symbol, and uri for the mint)
- Set the additional metadata in the mint
- Create the associated token account and mint the NFT to it and remove the mint authority
- Put all of that in one transaction and send it to the network
- Fetch and print the token account, the mint account, an the metadata to make sure that it is working correctly
This new function will take CreateNFTInputs
defined in the helpers.ts
file.
As a first step, let's create a new file src/nft-with-embedded-metadata.ts
and
paste the following:
import {
Keypair,
sendAndConfirmTransaction,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { CreateNFTInputs } from "./helpers";
import {
createInitializeInstruction,
createUpdateFieldInstruction,
pack,
TokenMetadata,
} from "@solana/spl-token-metadata";
import {
AuthorityType,
createAssociatedTokenAccountInstruction,
createInitializeMetadataPointerInstruction,
createInitializeMintInstruction,
createMintToCheckedInstruction,
createSetAuthorityInstruction,
ExtensionType,
getAccount,
getAssociatedTokenAddress,
getMint,
getMintLen,
getTokenMetadata,
LENGTH_SIZE,
TOKEN_2022_PROGRAM_ID,
TYPE_SIZE,
} from "@solana/spl-token";
export default async function createNFTWithEmbeddedMetadata(
inputs: CreateNFTInputs,
) {
const {
payer,
connection,
tokenName,
tokenSymbol,
tokenUri,
tokenAdditionalMetadata,
} = inputs;
// 0. Setup Mint
// 1. Create the metadata object
// 2. Allocate the mint
// 3. Initialize the metadata-pointer making sure that it points to the mint itself
// 4. Initialize the mint
// 5. Initialize the metadata inside the mint (that will set name, symbol, and uri for the mint)
// 6. Set the additional metadata in the mint
// 7. Create the associated token account and mint the NFT to it and remove the mint authority
// 8. Put all of that in one transaction and send it to the network
// 9. fetch and print the token account, the mint account, an the metadata to make sure that it is working correctly
}
Now let's fill in the gaps one by one.
For step 0 we create the mint's keypair, make sure our decimals for our NFT is 0 and the supply is 1.
// 0. Setup Mint
const mint = Keypair.generate();
const decimals = 0; // NFT should have 0 decimals
const supply = 1; // NFTs should have a supply of 1
Now let's construct the TokenMetadata
object interfaced from
@solana/spl-token-metadata
, and pass it all of our inputs.
Note we have to do some conversion of our tokenAdditionalMetadata
:
// 1. Create the metadata object
const metadata: TokenMetadata = {
mint: mint.publicKey,
name: tokenName,
symbol: tokenSymbol,
uri: tokenUri,
// additionalMetadata: [['customField', 'customValue']],
additionalMetadata: Object.entries(tokenAdditionalMetadata || []).map(
([key, value]) => [key, value],
),
};
Now we can create our first onchain instruction using
SystemProgram.createAccount
. To do this we need to know the size of our NFT's
mint account. Remember we're using two extensions for our NFT,
metadata pointer
and the metadata
extensions. Additionally, since the
metadata is 'embedded' using the metadata extension, it's variable length. So we
use a combination of getMintLen
, pack
and some hardcoded amounts to get our
final length.
Then we call getMinimumBalanceForRentExemption
to see how many lamports it
costs to spin up the account.
Finally, we put everything into the SystemProgram.createAccount
function to
get our first instruction:
// 2. Allocate the mint
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
const lamports = await connection.getMinimumBalanceForRentExemption(
mintLen + metadataLen,
);
const createMintAccountInstruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
lamports,
newAccountPubkey: mint.publicKey,
programId: TOKEN_2022_PROGRAM_ID,
space: mintLen,
});
The more information in the metadata, the more it costs.
Step 3 has us initializing the metadata pointer
extension. Let's do that by
calling the createInitializeMetadataPointerInstruction
function with the
metadata account point to our mint.
// 3. Initialize the metadata-pointer making sure that it points to the mint itself
const initMetadataPointerInstruction =
createInitializeMetadataPointerInstruction(
mint.publicKey,
payer.publicKey,
mint.publicKey, // Metadata account - points to itself
TOKEN_2022_PROGRAM_ID,
);
Next is the createInitializeMintInstruction
. Note that we do this before we
initialize the metadata.
// 4. Initialize the mint
const initMintInstruction = createInitializeMintInstruction(
mint.publicKey,
decimals,
payer.publicKey,
payer.publicKey,
TOKEN_2022_PROGRAM_ID,
);
Now we can initialize our metadata with the createInitializeInstruction
. We
pass in all of our NFT metadata except for our tokenAdditionalMetadata
, which
is covered in our next step.
// 5. Initialize the metadata inside the mint
const initMetadataInstruction = createInitializeInstruction({
programId: TOKEN_2022_PROGRAM_ID,
mint: mint.publicKey,
metadata: mint.publicKey,
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
mintAuthority: payer.publicKey,
updateAuthority: payer.publicKey,
});
In our NFT, we have tokenAdditionalMetadata
, and as we saw in the previous
step this cannot be set using the createInitializeInstruction
. So we have to
make an instruction to set each new additional field. We do this by calling
createUpdateFieldInstruction
for each of our entries in
tokenAdditionalMetadata
.
// 6. Set the additional metadata in the mint
const setExtraMetadataInstructions = [];
for (const attributes of Object.entries(tokenAdditionalMetadata || [])) {
setExtraMetadataInstructions.push(
createUpdateFieldInstruction({
updateAuthority: payer.publicKey,
metadata: mint.publicKey,
field: attributes[0],
value: attributes[1],
programId: TOKEN_2022_PROGRAM_ID,
}),
);
}
Now let's mint this NFT to ourselves, and then revoke the mint authority. This will make it a true NFT where there will ever only be one. We accomplish this with the following functions:
createAssociatedTokenAccountInstruction
createMintToCheckedInstruction
createSetAuthorityInstruction
// 7. Create the associated token account and mint the NFT to it and remove the mint authority
const ata = await getAssociatedTokenAddress(
mint.publicKey,
payer.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
);
const createATAInstruction = createAssociatedTokenAccountInstruction(
payer.publicKey,
ata,
payer.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
);
const mintInstruction = createMintToCheckedInstruction(
mint.publicKey,
ata,
payer.publicKey,
supply, // NFTs should have a supply of one
decimals,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// NFTs should have no mint authority so no one can mint any more of the same NFT
const setMintTokenAuthorityInstruction = createSetAuthorityInstruction(
mint.publicKey,
payer.publicKey,
AuthorityType.MintTokens,
null,
undefined,
TOKEN_2022_PROGRAM_ID,
);
Now, let's bundle all of our transactions together and send it out to Solana. It is very important to note that order matters here.
// 8. Put all of that in one transaction and send it to the network.
const transaction = new Transaction().add(
createMintAccountInstruction,
initMetadataPointerInstruction,
initMintInstruction,
initMetadataInstruction,
...setExtraMetadataInstructions,
createATAInstruction,
mintInstruction,
setMintTokenAuthorityInstruction,
);
const transactionSignature = await sendAndConfirmTransaction(
connection,
transaction,
[payer, mint],
);
Lastly, let's fetch and print out all of the information about our NFT so we know everything worked.
// 9. fetch and print the token account, the mint account, an the metadata to make sure that it is working correctly.
// Fetching the account
const accountDetails = await getAccount(
connection,
ata,
"finalized",
TOKEN_2022_PROGRAM_ID,
);
console.log("Associate Token Account =====>", accountDetails);
// Fetching the mint
const mintDetails = await getMint(
connection,
mint.publicKey,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log("Mint =====>", mintDetails);
// Since the mint stores the metadata in itself, we can just get it like this
const onChainMetadata = await getTokenMetadata(connection, mint.publicKey);
// Now we can see the metadata coming with the mint
console.log("onchain metadata =====>", onChainMetadata);
// And we can even get the off-chain json now
if (onChainMetadata && onChainMetadata.uri) {
const offChainMetadata = await fetch(onChainMetadata.uri).then(res =>
res.json(),
);
console.log("Mint off-chain metadata =====>", offChainMetadata);
}
Putting it all together you get the following in
src/nft-with-embedded-metadata.ts
:
import {
Keypair,
sendAndConfirmTransaction,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { CreateNFTInputs } from "./helpers";
import {
createInitializeInstruction,
createUpdateFieldInstruction,
pack,
TokenMetadata,
} from "@solana/spl-token-metadata";
import {
AuthorityType,
createAssociatedTokenAccountInstruction,
createInitializeMetadataPointerInstruction,
createInitializeMintInstruction,
createMintToCheckedInstruction,
createSetAuthorityInstruction,
ExtensionType,
getAccount,
getAssociatedTokenAddress,
getMint,
getMintLen,
getTokenMetadata,
LENGTH_SIZE,
TOKEN_2022_PROGRAM_ID,
TYPE_SIZE,
} from "@solana/spl-token";
export default async function createNFTWithEmbeddedMetadata(
inputs: CreateNFTInputs,
) {
const {
payer,
connection,
tokenName,
tokenSymbol,
tokenUri,
tokenAdditionalMetadata,
} = inputs;
// 0. Setup Mint
const mint = Keypair.generate();
const decimals = 0; // NFT should have 0 decimals
const supply = 1; // NFTs should have a supply of one
// 1. Create the metadata object
const metadata: TokenMetadata = {
mint: mint.publicKey,
name: tokenName,
symbol: tokenSymbol,
uri: tokenUri,
// additionalMetadata: [['customField', 'customValue']],
additionalMetadata: Object.entries(tokenAdditionalMetadata || []).map(
([key, value]) => [key, value],
),
};
// 2. Allocate the mint
const mintLen = getMintLen([ExtensionType.MetadataPointer]);
const metadataLen = TYPE_SIZE + LENGTH_SIZE + pack(metadata).length;
const lamports = await connection.getMinimumBalanceForRentExemption(
mintLen + metadataLen,
);
const createMintAccountInstruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey,
lamports,
newAccountPubkey: mint.publicKey,
programId: TOKEN_2022_PROGRAM_ID,
space: mintLen,
});
// 3. Initialize the metadata-pointer making sure that it points to the mint itself
const initMetadataPointerInstruction =
createInitializeMetadataPointerInstruction(
mint.publicKey,
payer.publicKey,
mint.publicKey, // Metadata account - points to itself
TOKEN_2022_PROGRAM_ID,
);
// 4. Initialize the mint
const initMintInstruction = createInitializeMintInstruction(
mint.publicKey,
decimals,
payer.publicKey,
payer.publicKey,
TOKEN_2022_PROGRAM_ID,
);
// 5. Initialize the metadata inside the mint
const initMetadataInstruction = createInitializeInstruction({
programId: TOKEN_2022_PROGRAM_ID,
mint: mint.publicKey,
metadata: mint.publicKey,
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
mintAuthority: payer.publicKey,
updateAuthority: payer.publicKey,
});
// 6. Set the additional metadata in the mint
const setExtraMetadataInstructions = [];
for (const attributes of Object.entries(tokenAdditionalMetadata || [])) {
setExtraMetadataInstructions.push(
createUpdateFieldInstruction({
updateAuthority: payer.publicKey,
metadata: mint.publicKey,
field: attributes[0],
value: attributes[1],
programId: TOKEN_2022_PROGRAM_ID,
}),
);
}
// 7. Create the associated token account and mint the NFT to it and remove the mint authority
const ata = await getAssociatedTokenAddress(
mint.publicKey,
payer.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
);
const createATAInstruction = createAssociatedTokenAccountInstruction(
payer.publicKey,
ata,
payer.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
);
const mintInstruction = createMintToCheckedInstruction(
mint.publicKey,
ata,
payer.publicKey,
supply, // NFTs should have a supply of one
decimals,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// NFTs should have no mint authority so no one can mint any more of the same NFT
const setMintTokenAuthorityInstruction = createSetAuthorityInstruction(
mint.publicKey,
payer.publicKey,
AuthorityType.MintTokens,
null,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// 8. Put all of that in one transaction and send it to the network.
const transaction = new Transaction().add(
createMintAccountInstruction,
initMetadataPointerInstruction,
initMintInstruction,
initMetadataInstruction,
...setExtraMetadataInstructions, // Destructuring extra metadata fields
createATAInstruction,
mintInstruction,
setMintTokenAuthorityInstruction,
);
const transactionSignature = await sendAndConfirmTransaction(
connection,
transaction,
[payer, mint],
);
// 9. fetch and print the token account, the mint account, an the metadata to make sure that it is working correctly.
// Fetching the account
const accountDetails = await getAccount(
connection,
ata,
"finalized",
TOKEN_2022_PROGRAM_ID,
);
console.log("Associate Token Account =====>", accountDetails);
// Fetching the mint
const mintDetails = await getMint(
connection,
mint.publicKey,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log("Mint =====>", mintDetails);
// Since the mint stores the metadata in itself, we can just get it like this
const onChainMetadata = await getTokenMetadata(connection, mint.publicKey);
// Now we can see the metadata coming with the mint
console.log("onchain metadata =====>", onChainMetadata);
// And we can even get the off-chain JSON now
if (onChainMetadata && onChainMetadata.uri) {
const offChainMetadata = await fetch(onChainMetadata.uri).then(res =>
res.json(),
);
console.log("Mint off-chain metadata =====>", offChainMetadata);
}
}
3. Call Create NFT Function #
Let's put everything together in src/index.ts
.
Go back to src/index.ts
, and import the function
createNFTWithEmbeddedMetadata
from the file we just created.
import createNFTWithEmbeddedMetadata from "./nft-with-embedded-metadata";
Then call it at the end of the main function and pass the required parameters.
await createNFTWithEmbeddedMetadata({
payer,
connection,
tokenName,
tokenSymbol,
tokenUri,
});
src/index.ts
file should look like this:
import { Connection } from "@solana/web3.js";
import { initializeKeypair, uploadOffChainMetadata } from "./helpers";
import createNFTWithEmbeddedMetadata from "./nft-with-embedded-metadata";
import dotenv from "dotenv";
dotenv.config();
const connection = new Connection("http://127.0.0.1:8899", "finalized");
const payer = await initializeKeypair(connection);
const imagePath = "NFT.png";
const tokenName = "NFT Name";
const tokenDescription = "This is a cool Token Extension NFT";
const tokenSymbol = "TTT";
const tokenUri = await uploadOffChainMetadata({
connection,
payer,
tokenName,
tokenDescription,
tokenSymbol,
imagePath,
});
// You can log the URI here and run the code to test it
console.log("Token URI:", tokenUri);
await createNFTWithEmbeddedMetadata({
payer,
connection,
tokenName,
tokenSymbol,
tokenUri,
});
Run the program one more time to see your NFT and metadata.
npm run start
You did it! You've made an NFT using the metadata
and metadata pointer
extensions.
If you run into any problems, check out the solution.
Challenge #
Taking what you've learned here, go and create your own NFT or SFT.