Include token metadata directly inside the token mint account.
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 the metadata
extension.metadata
mint extension allows embedding of metadata directly into mint
accounts through the Token Extensions Program. This is always accompanied with
a self-referencing metadata pointer
. This facilitates embedding
comprehensive token information at the minting stage.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.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 the metadata
extension.authority
: The authority that can set the metadata address.createInitializeMetadataPointerInstruction
createUpdateMetadataPointerInstruction
getMetadataPointerState
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)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)getMetadataPointerState
function will return the MetadataPointer
state
for the given Mint
object. We can get this using the getMint
function.
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 Nexis Native Chain 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 @nexis-network/spl-token
library.
The metadata-pointer extension increases the size of the mint account by adding
two new fields: metadataAddress
and authority
.
mint
with the metadata pointer, we need several
instructions in a particular order:
mint
account, which reserves space on the blockchain with
SystemProgram.createAccount
createInitializeMetadataPointerInstruction
createInitializeMintInstruction
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 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:
@nexis-network/spl-token-metadata
library has been
updated with the following functions to help out:
createInitializeInstruction
createUpdateFieldInstruction
createRemoveKeyInstruction
createUpdateAuthorityInstruction
createEmitInstruction
pack
unpack
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 datacreateInitializeInstruction
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)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 in
Field
s or a custom field stored in the additional_metadata
fieldvalue
: the updated value of the fieldprogramId
: the SPL Token program Id (in this case it will be the Token
Extension program Id)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 existcreateUpdateAuthorityInstruction
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)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 metadatapack
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.
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)mint
accountmint
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:
LENGTH_SIZE
and TYPE_SIZE
constants from the @nexis-network/spl-token
library - these are sizes associated with mint extensions that are usually
added with the call getMintLen
, but since the metadata extension is
variable length, they need to be added manuallycreateUpdateFieldInstruction
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 @nexis-network/spl-token
library:mint
with the metadata and metadata
pointer, we need several instructions in a particular order:
mint
account which reserves space on the blockchain with
SystemProgram.createAccount
createInitializeMetadataPointerInstruction
createInitializeMintInstruction
createInitializeInstruction
(this ONLY sets
the basic metadata fields)createUpdateFieldInstruction
(one
field per call)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:metadata
and metadata pointer
extensions.
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.
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, Nexis Native Chain’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.
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:
keypairPath
parameter to initializeKeypair
- path can be gotten by
running solana config get
in your terminalsolana address
in your terminalcat.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://nexis.network/attributes
= { species: 'Cat' breed: 'Cool' }
uploadOffChainMetadata
to get the uploaded metadata uri.
When we put all of this together, the index.ts
file will look as follows:
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.
createNFTWithEmbeddedMetadata
in a new file called
src/nft-with-embedded-metadata.ts
.
This function will create an NFT by doing the following:
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:
TokenMetadata
object interfaced from
@nexis-network/spl-token-metadata
, and pass it all of our inputs.
Note we have to do some conversion of our tokenAdditionalMetadata
:
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:
metadata pointer
extension. Let’s do that by
calling the createInitializeMetadataPointerInstruction
function with the
metadata account point to our mint.
createInitializeMintInstruction
. Note that we do this before we
initialize the metadata.
createInitializeInstruction
. We
pass in all of our NFT metadata except for our tokenAdditionalMetadata
, which
is covered in our next step.
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
.
createAssociatedTokenAccountInstruction
createMintToCheckedInstruction
createSetAuthorityInstruction
src/nft-with-embedded-metadata.ts
:
src/index.ts
.
Go back to src/index.ts
, and import the function
createNFTWithEmbeddedMetadata
from the file we just created.
src/index.ts
file should look like this:
metadata
and metadata pointer
extensions.
If you run into any problems, check out the
solution.