Summary
- 'token groups' are commonly used to implement NFT collections.
- The
group pointer
extension sets a group account on the token mint, to hold token group information. - The
group
extension allows us to save group data within the mint itself. - The
member pointer
extension sets an individual member account on the token mint, to hold information about the token's membership within a group. - The
member
extension allows us to save member data within the mint itself.
Overview
SPL tokens are valuable alone but can be combined for extra functionality. We
can do this in the Token Extensions Program by combining the group
,
group pointer
, member
, and member pointer
extensions. The most common use
case for these extensions is to create a collection of NFTs.
To create a collection of NFTs we need two parts: the "collection" NFT and all
of the NFTs within the collection. We can do this entirely using token
extensions. The "collection" NFT can be a single mint combining the metadata
,
metadata pointer
, group
, and group pointer
extensions. And then each
individual NFT within the collection can be an array of mints combining the
metadata
, metadata pointer
, member
, and member pointer
extensions.
Although NFT collections are a common use-case, groups and members can be applied to any token type.
A quick note on group pointer
vs group
. The group pointer
extension saves
the address of any onchain account that follows to the
Token-Group Interface.
While the group
extension saves the Token-Group Interface data directly within
the mint account. Generally, these are used together where the group pointer
points to the mint itself. The same is true for member pointer
vs member
,
but with the member data.
NOTE: A group can have many members, but a member can only belong to one group.
Group and Group Pointer
The group
and group pointer
extensions define a token group. The onchain
data is as follows:
update_authority
: The authority that can sign to update the group.mint
: The mint of the group token.size
: The current number of group members.max_size
: The maximum number of group members.
Creating a mint with group and group pointer
Creating a mint with the group
and group pointer
involves four instructions:
SystemProgram.createAccount
createInitializeGroupPointerInstruction
createInitializeMintInstruction
createInitializeGroupInstruction
The first instruction SystemProgram.createAccount
allocates space on the
blockchain for the mint account. However like all Token Extensions Program
mints, we need to calculate the size and cost of the mint. This can be
accomplished by using getMintLen
and getMinimumBalanceForRentExemption
. In
this case, we'll call getMintLen
with only the ExtensionType.GroupPointer
.
Then we add TOKEN_GROUP_SIZE
to the mint length to account for the group data.
To get the mint length and create account instruction, do the following:
The second instruction createInitializeGroupPointerInstruction
initializes the
group pointer. It takes the mint, optional authority that can set the group
address, address that holds the group and the owning program as it's arguments.
The third instruction createInitializeMintInstruction
initializes the mint.
The fourth instruction createInitializeGroupInstruction
actually initializes
the group and stores the configuration on the group account.
Finally, we add the instructions to the transaction and submit it to the Solana network.
Update group authority
To update the authority of a group, we just need the
tokenGroupUpdateGroupAuthority
function.
Update max size of a group
To update the max size of a group we just need the
tokenGroupUpdateGroupMaxSize
function.
Member and Member Pointer
The member
and member pointer
extensions define a token member. The onchain
data is as follows:
mint
: The mint of the member token.group
: The address of the group account.member_number
: The member number (index within the group).
Creating a mint with member pointer
Creating a mint with the member pointer
and member
extensions involves four
instructions:
SystemProgram.createAccount
createInitializeGroupMemberPointerInstruction
createInitializeMintInstruction
createInitializeMemberInstruction
The first instruction SystemProgram.createAccount
allocates space on the
blockchain for the mint account. However, like all Token Extensions Program
mints, we need to calculate the size and cost of the mint. This can be
accomplished by using getMintLen
and getMinimumBalanceForRentExemption
. In
this case, we'll call getMintLen
with the ExtensionType.GroupMemberPointer
.
Then we have to add TOKEN_GROUP_MEMBER_SIZE
to the mint length to account for
the member data.
To get the mint length and create account instruction, do the following:
The second instruction createInitializeGroupMemberPointerInstruction
initializes the group member pointer. It takes the mint, optional authority that
can set the group address, address that holds the group, and the owning program
as its arguments.
The third instruction createInitializeMintInstruction
initializes the mint.
The fourth instruction createInitializeMemberInstruction
actually initializes
the member and stores the configuration on the member account. This function
takes the group address as an argument and associates the member with that
group.
Finally, we add the instructions to the transaction and submit it to the Solana network.
Fetch group and member data
Get group pointer state
To retrieve the state of the group pointer
for a mint, we need to fetch the
account using getMint
and then parse this data using the
getGroupPointerState
function. This returns us the GroupPointer
struct.
To get the GroupPointer
data, call the following:
Get group state
To retrieve the group state for a mint, we need to fetch the account using
getMint
and then parse this data using the getTokenGroupState
function. This
returns the TokenGroup
struct.
To get the TokenGroup
data, call the following:
Get group member pointer state
To retrieve the member pointer
state for a mint, we fetch the mint with
getMint
and then parse with getGroupMemberPointerState
. This returns us the
GroupMemberPointer
struct.
To get the GroupMemberPointer
data, call the following:
Get group member state
To retrieve a mint's member
state, we fetch the mint with getMint
and then
parse with getTokenGroupMemberState
. This returns the TokenGroupMember
struct.
To get the TokenGroupMember
data, call the following:
Lab
In this lab we'll create a Cool Cats NFT collection using the group
,
group pointer
, member
and member pointer
extensions in conjunction with
the metadata
and metadata pointer
extensions.
The Cool Cats NFT collection will have a group NFT with three member NFTs within it.
1. Getting started
To get started, clone
this repository's
starter
branch.
The starter
code comes with:
index.ts
: creates a connection object and callsinitializeKeypair
. This is where we will write our script.assets
: folder which contains the image for our NFT collection.helper.ts
: helper functions for uploading metadata.
2. Run validator node
For the sake of this guide, we'll be running our own validator node.
In a separate terminal, run the following command: solana-test-validator
. This
will run the node and also log out some keys and values. The value we need to
retrieve and use in our connection is the JSON RPC URL, which in this case is
http://127.0.0.1:8899
. We then use that in the connection to specify to use
the local RPC URL.
const connection = new Connection("http://127.0.0.1:8899", "confirmed");
With the validator setup correctly, you may run index.ts
and confirm
everything is working.
3. Setup group metadata
Before creating our group NFT, we must prepare and upload the group metadata. We
are using devnet Irys (Arweave) to upload the image and metadata. This
functionality is provided for you in the helpers.ts
.
For ease of this lesson, we've provided assets for the NFTs in the assets
directory.
If you'd like to use your own files and metadata feel free!
To get our group metadata ready we have to do the following:
- We need to format our metadata for upload using the
LabNFTMetadata
interface fromhelper.ts
- Call the
uploadOffChainMetadata
fromhelpers.ts
- Format everything including the resulting uri from the previous step into the
We need to format our metadata for upload (LabNFTMetadata
), upload the image
and metadata (uploadOffChainMetadata
), and finally format everything into the
TokenMetadata
interface from the @solana/spl-token-metadata
library.
Note: We are using devnet Irys, which is free to upload to under 100kb.
Feel free to run the script and make sure everything uploads.
3. Create a mint with group and group pointer
Let's create the group NFT by creating a mint with the metadata
,
metadata pointer
, group
and group pointer
extensions.
This NFT is the visual representation of our collection.
Let's first define the inputs to our new function createTokenGroup
:
connection
: Connection to the blockchainpayer
: The keypair paying for the transactionmintKeypair
: The mint keypairdecimals
: The mint decimals ( 0 for NFTs )maxMembers
: The maximum number of members allowed in the groupmetadata
: The metadata for the group mint
To make our NFT, we will store the metadata directly on the mint account using
the metadata
and metadata pointer
extensions. We'll also save some info
about the group with the group
and group pointer
extensions.
To create our group NFT, we need the following instructions:
SystemProgram.createAccount
: Allocates space on Solana for the mint account. We can get themintLength
andmintLamports
usinggetMintLen
andgetMinimumBalanceForRentExemption
respectively.createInitializeGroupPointerInstruction
: Initializes the group pointercreateInitializeMetadataPointerInstruction
: Initializes the metadata pointercreateInitializeMintInstruction
: Initializes the mintcreateInitializeGroupInstruction
: Initializes the groupcreateInitializeInstruction
: Initializes the metadata
Finally, we need to add all of these instructions to a transaction and send it
to the Solana network, and return the signature. We can do this by calling
sendAndConfirmTransaction
.
Now that we have our function, let's call it in our index.ts
file.
Before we run the script, lets fetch the newly created group NFT and print it's
contents. Let's do this in index.ts
:
Now we can run the script and see the group NFT we created.
4. Setup member NFT Metadata
Now that we've created our group NFT, we can create the member NFTs. But before we actually create them, we need to prepare their metadata.
The flow is the exact same to what we did with the group NFT.
- We need to format our metadata for upload using the
LabNFTMetadata
interface fromhelper.ts
- Call the
uploadOffChainMetadata
fromhelpers.ts
- Format everything including the resulting uri from the previous step into the
TokenMetadata
interface from the@solana/spl-token-metadata
library.
However, since we have three members, we'll loop through each step for each member.
First, let's define the metadata for each member:
Now let's loop through each member and upload their metadata.
Finally, let's format the metadata for each member into the TokenMetadata
interface:
Note: We'll want to carry over the keypair since we'll need it to create the member NFTs.
5. Create member NFTs
Just like the group NFT, we need to create the member NFTs. Let's do this in a
new file called create-member.ts
. It will look very similar to the
create-group.ts
file, except we'll use the member
and member pointer
extensions instead of the group
and group pointer
extensions.
First, let's define the inputs to our new function createTokenMember
:
connection
: Connection to the blockchainpayer
: The keypair paying for the transactionmintKeypair
: The mint keypairdecimals
: The mint decimals ( 0 for NFTs )metadata
: The metadata for the group mintgroupAddress
: The address of the group account - in this case it's the group mint itself
Just like the group NFT, we need the following instructions:
SystemProgram.createAccount
: Allocates space on Solana for the mint account. We can get themintLength
andmintLamports
usinggetMintLen
andgetMinimumBalanceForRentExemption
respectively.createInitializeGroupMemberPointerInstruction
: Initializes the member pointercreateInitializeMetadataPointerInstruction
: Initializes the metadata pointercreateInitializeMintInstruction
: Initializes the mintcreateInitializeMemberInstruction
: Initializes the membercreateInitializeInstruction
: Initializes the metadata
Finally, we need to add these instructions to a transaction, send it to the
Solana network, and return the signature. We can do this by calling
sendAndConfirmTransaction
.
Let's add our new function to index.ts
and call it for each member:
Let's fetch our newly created member NFTs and display their contents.
Lastly, let's run the script and see our full collection of NFTs!
That's it! If you're having troubles feel free to check out the solution
branch in the repository.
Challenge
Go create a NFT collection of your own using the the group
, group pointer
,
member
and member pointer
extensions.