How to Create a Fungible SPL token with the New Metaplex Token Standard
18 min read
Overview
Are you creating a Whitelist token for your upcoming NFT mint? Or want to launch a fungible token for your next great dApp? Solana and Metaplex make it easy to do so right from your terminal!
On June 20, 2022, Solana deprecated the Solana Token List, the repository which housed metadata with all fungible SPL tokens. The list has been replaced by Metaplex's Fungible Token Standard. If you're familiar with the old standard or just getting started with your first SPL token, this guide is for you.
What You Will Do
In this guide, you will create a wallet (and airdrop some SOL), create fungible token metadata using the Metaplex standard, upload token metadata to Arweave, and mint a new fungible SPL token on Solana.
What You Will Need
- Nodejs installed (version 16.15 or higher)
- npm or yarn installed (We will be using yarn to initialize our project and install the necessary packages. Feel free to use npm instead if that’s your preferred package manager)
- Typescript experience and ts-node installed
- Solana Web3
- Solana SPL Token Library
- Metaplex Foundation JS SDK](https://www.npmjs.com/package/@metaplex-foundation/js)
- Metaplex Foundation MPL Token Metadata Library
Set Up Your Environment
Create a new project directory in your terminal with:
mkdir mint-fungible-spl
cd mint-fungible-spl
Create two files, wallet.ts and mint.ts. We will use wallet.ts to create a new dev wallet and airdrop some Solana for testing. We'll use mint.ts to mint a new SPL token and upload our token metadata.
echo > {wallet,mint}.ts
Initialize your project with the "yes" flag to use default values for your new package:
yarn init --yes
#or
npm init --yes
Create a tsconfig.json file:
tsc --init
Open tsconfig.json and uncomment (or add) this to your file:
"resolveJsonModule": true
This will allow us to import .json files into our repository, which will be important later when we want to generate a Keypair from a PrivateKey.
Also double check that esModuleInterop is set to true to allow for us to use imports.
Install Solana Web3 dependencies:
yarn add @solana/web3.js @metaplex-foundation/mpl-token-metadata @metaplex-foundation/js @solana/spl-token
#or
npm install @solana/web3.js @metaplex-foundation/mpl-token-metadata @metaplex-foundation/js @solana/spl-token
Your environment should look something like this:
Alright! We're all ready to go.
Set Up Your Quicknode Endpoint
To build on Solana, you'll need an API endpoint to connect with the network. You're welcome to use public nodes or deploy and manage your own infrastructure; however, if you'd like 8x faster response times, you can leave the heavy lifting to us. See why over 50% of projects on Solana choose QuickNode and sign up for a free account here.
We're going to use a Solana Devnet node. Copy the HTTP Provider link:
Create a Wallet and Airdrop SOL
In order to mint a fungible SPL token, we'll first want to create a Devnet wallet and airdrop SOL into it. If you already have a paper wallet, save it to your project directory as guideSecret.json. If it needs some devnet SOL, you can request some with the form below:
If you don't have a paper wallet, we'll programmatically generate a new one. Open wallet.ts and paste the following code in. We'll break it down in the next section.
import { Keypair, LAMPORTS_PER_SOL, Connection } from "@solana/web3.js";
import * as fs from 'fs';
//STEP 1 - Connect to Solana Network
const endpoint = 'https://example.solana-devnet.quiknode.pro/000000/'; //Replace with your RPC Endpoint
const solanaConnection = new Connection(endpoint);
//STEP 2 - Generate a New Solana Wallet
const keypair = Keypair.generate();
console.log(`Generated new KeyPair. Wallet PublicKey: `, keypair.publicKey.toString());
//STEP 3 - Write Wallet Secret Key to a .JSON
const secret_array = keypair.secretKey
.toString() //convert secret key to string
.split(',') //delimit string by commas and convert to an array of strings
.map(value=>Number(value)); //convert string values to numbers inside the array
const secret = JSON.stringify(secret_array); //Covert to JSON string
fs.writeFile('guideSecret.json', secret, 'utf8', function(err) {
if (err) throw err;
console.log('Wrote secret key to guideSecret.json.');
});
//STEP 4 - Airdrop 1 SOL to new wallet
(async()=>{
const airdropSignature = solanaConnection.requestAirdrop(
keypair.publicKey,
LAMPORTS_PER_SOL,
);
try{
const txId = await airdropSignature;
console.log(`Airdrop Transaction Id: ${txId}`);
console.log(`https://explorer.solana.com/tx/${txId}?cluster=devnet`)
}
catch(err){
console.log(err);
}
})()
This script will perform 4 tasks:
- Connect to the Solana Network (Make sure you replace endpoint with your Quicknode Endpoint URL that you saved in the previous step).
- Generate a new Wallet Keypair.
- Write the Secret Key to a .json file that we'll use in the next step. Lines 13-18 are necessary to format the key as an array of numbers. Lines 20-22 use fs to export the array to a .json file.
- Airdrop 1 SOL to the new Wallet.
Go ahead and run this script to create a new wallet and aidrop it 1 SOL:
ts-node wallet.ts
You should see a new file, guideSecret.json in your project folder and a terminal log like this:
Generated new KeyPair. Wallet PublicKey: DQmULZZBnnyZeoYsMLaNmtY5ZgTk5LJtBmM1ApqbqWKe
Wrote secret key to guideSecret.json.
Airdrop Transaction Id: 3mqvvtHQbPyHrtEA6nQT9z7YtKbjP4YzjYE7xoMq7XG6WU8v6WouvUPiSFnUbBhtzX7c1SE859fXWbHPTSHXuu
https://explorer.solana.com/tx/3mqvvtHQbPyHrtEA6nQT9z7YtKbjP4YzjYE7xoMq7XG6WU8v6WouvUPiSFnUbBhtzX7c1SE859fXWbHPTSHXuu?cluster=devnet
Let's make our token!
Build a Mint Tool
Import Dependencies
Open up mint.ts and import the following dependencies on line 1:
import { Transaction, SystemProgram, Keypair, Connection, PublicKey, sendAndConfirmTransaction } from "@solana/web3.js";
import { MINT_SIZE, TOKEN_PROGRAM_ID, createInitializeMintInstruction, getMinimumBalanceForRentExemptMint, getAssociatedTokenAddress, createAssociatedTokenAccountInstruction, createMintToInstruction } from '@solana/spl-token';
import { DataV2, createCreateMetadataAccountV3Instruction } from '@metaplex-foundation/mpl-token-metadata';
import { bundlrStorage, keypairIdentity, Metaplex, UploadMetadataInput } from '@metaplex-foundation/js';
import secret from './guideSecret.json';
We'll cover these as we get to them in the guide, but we do want to note that final import, secret, which is importing the .json we created in the previous step.
Establish Solana Connection
Create a Connection to the Solana network but pasting your adding your Quicknode Endpoint URL to the code below and pasting it just below your imports:
const endpoint = 'https://example.solana-devnet.quiknode.pro/000000/'; //Replace with your RPC Endpoint
const solanaConnection = new Connection(endpoint);
Establish a Metaplex istance with your connection and secret key:
const userWallet = Keypair.fromSecretKey(new Uint8Array(secret));
const metaplex = Metaplex.make(solanaConnection)
.use(keypairIdentity(userWallet))
.use(bundlrStorage({
address: 'https://devnet.bundlr.network',
providerUrl: endpoint,
timeout: 60000,
}));
Create Mint Configuration and Metadata
Let's add some detail about the token we want to create. Below your Solana Connection, create a variable called MINT_CONFIG that we'll use to define the number of decimals our token will have and the number of tokens we'd like to mint:
const MINT_CONFIG = {
numDecimals: 6,
numberTokens: 1337
}
*Note: setting numDecimals to 0 results in a token that cannot be subdivided. This might be relevant for something like a membership or whitelist mint token.
*Now let's define our metadata. Solana has recently adopted Metaplex's Fungible Token Standard, which requires a name, symbol, description, and image (all are string values). Using the standard with your token mint will enable major platforms like Phantom Wallet or Solana Explorer to easily recognize your token and make it viewable by their users.
After MINT_CONFIG, create a new object called MY_TOKEN_METADATA of type, UploadMetadataInput and define your token:
const MY_TOKEN_METADATA: UploadMetadataInput = {
name: "Test Token",
symbol: "TEST",
description: "This is a test token!",
image: "https://URL_TO_YOUR_IMAGE.png" //add public URL to image you'd like to use
}
Later in this excercise, we'll use Metaplex's uploadMetadata method to load this metadata to Arweave.
Finally, we'll need to create a second metadata variable that will be loading onto Solana into Metaplex's Program. Create a new variable, ON_CHAIN_METADATA:
const ON_CHAIN_METADATA = {
name: MY_TOKEN_METADATA.name,
symbol: MY_TOKEN_METADATA.symbol,
uri: 'TO_UPDATE_LATER',
sellerFeeBasisPoints: 0,
creators: null,
collection: null,
uses: null
} as DataV2;
You'll see that we use the name and symbol from MY_TOKEN_METADATA. We define a key, uri as a string that we'll update later. This will need to be replaced to a URL where we upload our MY_TOKEN_METADATA object. Set sellerFeeBasisPoints to 0 and set creators, collection, and uses to null since we won't need those for a fungible token.
Finally, create a function to upload MY_TOKEN_METADATA to Arweave.
/**
*
* @param wallet Solana Keypair
* @param tokenMetadata Metaplex Fungible Token Standard object
* @returns Arweave url for our metadata json file
*/
const uploadMetadata = async (tokenMetadata: UploadMetadataInput): Promise<string> => {
//Upload to Arweave
const { uri } = await metaplex.nfts().uploadMetadata(tokenMetadata);
console.log(`Arweave URL: `, uri);
return uri;
}
Note: if you are using a version of Metaplex JS before 0.17, you may need to add .run()
to the end of your uploadMetadata() call.
The upLoadMetadata function will execute upload using uploadMetadata and returning an Arweave URL.
Great! If you're following along, your code should look something like this:
import { Transaction, SystemProgram, Keypair, Connection, PublicKey, sendAndConfirmTransaction } from "@solana/web3.js";
import { MINT_SIZE, TOKEN_PROGRAM_ID, createInitializeMintInstruction, getMinimumBalanceForRentExemptMint, getAssociatedTokenAddress, createAssociatedTokenAccountInstruction, createMintToInstruction } from '@solana/spl-token';
import { DataV2, createCreateMetadataAccountV3Instruction } from '@metaplex-foundation/mpl-token-metadata';
import { bundlrStorage, keypairIdentity, Metaplex, UploadMetadataInput } from '@metaplex-foundation/js';
import secret from './guideSecret.json';
const endpoint = 'https://example.solana-devnet.quiknode.pro/000000/'; //Replace with your RPC Endpoint
const solanaConnection = new Connection(endpoint);
const userWallet = Keypair.fromSecretKey(new Uint8Array(secret));
const metaplex = Metaplex.make(solanaConnection)
.use(keypairIdentity(userWallet))
.use(bundlrStorage({
address: 'https://devnet.bundlr.network',
providerUrl: endpoint,
timeout: 60000,
}));
const MINT_CONFIG = {
numDecimals: 6,
numberTokens: 1337
}
const MY_TOKEN_METADATA: UploadMetadataInput = {
name: "Test Token",
symbol: "TEST",
description: "This is a test token!",
image: "https://URL_TO_YOUR_IMAGE.png" //add public URL to image you'd like to use
}
const ON_CHAIN_METADATA = {
name: MY_TOKEN_METADATA.name,
symbol: MY_TOKEN_METADATA.symbol,
uri: 'TO_UPDATE_LATER',
sellerFeeBasisPoints: 0,
creators: null,
collection: null,
uses: null
} as DataV2;
/**
*
* @param wallet Solana Keypair
* @param tokenMetadata Metaplex Fungible Token Standard object
* @returns Arweave url for our metadata json file
*/
const uploadMetadata = async(tokenMetadata: UploadMetadataInput):Promise<string> => {
//Upload to Arweave
const { uri } = await metaplex.nfts().uploadMetadata(tokenMetadata);
console.log(`Arweave URL: `, uri);
return uri;
}
Assemble Mint Transaction
Now we need to create a function that will create a Solana Transaction for our Mint. We've modeled our transaction after Solana's demonstration site. This is pretty similar to minting an SPL token previously, but now requires an additional step of adding Metadata to the token.
After your uploadMetadata function, create a new function, createNewMintTransaction and pass in 6 parameters: connection, payer, mintKeypair, destinationWallet, mintAuthority, freezeAuthority:
const createNewMintTransaction = async (connection:Connection, payer:Keypair, mintKeypair: Keypair, destinationWallet: PublicKey, mintAuthority: PublicKey, freezeAuthority: PublicKey)=>{
}
Note: if you want to use one wallet for payer, destination, mint authority, and freeze authority, you can use just the payer parameter, but we've separated this out in case you want to have different wallets serve different functions.
Inside our function, we'll need to get 3 values that will be necessary for our transaction:
- Calculate number of lamports required for our Token Mint Account. Solana has a built in method for this, getMinimumBalanceForRentExemptMint.
- Get the Metaplex program derived address ("PDA") for our token mint, using the Metaplex method,
.nfts().pdas().metadata()
. - Finally, we'll want to define our destination token address that we will pass into our createAssociatedTokenAccount instructions. We'll call our mint address and destination owner wallet into the Solana method, getAssociatedTokenAddress.
//Get the minimum lamport balance to create a new account and avoid rent payments
const requiredBalance = await getMinimumBalanceForRentExemptMint(connection);
//metadata account associated with mint
const metadataPDA = await metaplex.nfts().pdas().metadata({ mint: mintKeypair.publicKey });
//get associated token account of your wallet
const tokenATA = await getAssociatedTokenAddress(mintKeypair.publicKey, destinationWallet);
Now let's create our Transactions. Solana allows you to append multiple transactions instructions to a single Transaction using .add().
Create a new variable, createNewTokenTransaction:
const createNewTokenTransaction = new Transaction().add(
)
For our new mint, we're going to be stringing together 5 sets of instructions (add each of these in sequence inside of the .add() in your createNewTokenTransaction variable):
1. createAccount - Create new mint account. We must pass a CreateAccountParams into this instruction, which is an object comprised of fromPubKey (our account payer), newAccountPubkey (our new mint address), lamports (requiredBalance we calculated earlier will be transferred to the new account to avoid rent exemption), space (MINT_SIZE an import from spl-token library to ensure we allocate the correct amount of space to the new account), and programId (TOKEN_PROGRAM_ID imported from spl-token library).
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: MINT_SIZE,
lamports: requiredBalance,
programId: TOKEN_PROGRAM_ID,
}),
2.createInitializeMintInstruction - Initialize the new mint account. Here we'll pass mint (our mint public key), decimals (number of decimals in our MINT_CONFIG), mintAuthority(our mintAuthority parameter), freezeAuthority (our freezeAuthority parameter), and programId (TOKEN_PROGRAM_ID imported from spl-token library).
createInitializeMintInstruction(
mintKeypair.publicKey, //Mint Address
MINT_CONFIG.numDecimals, //Number of Decimals of New mint
mintAuthority, //Mint Authority
freezeAuthority, //Freeze Authority
TOKEN_PROGRAM_ID),
3. createAssociatedTokenAccountInstruction - Create a new token account for the new mint in your destination wallet. We will pass payer from our parameters, associatedToken (the tokenATA variable we defined earlier, which will be the token account where our tokens will ultimately be deposited), owner (the wallet owner of the new token account, our payer), and the mint (the public key of the mint we created).
createAssociatedTokenAccountInstruction(
payer.publicKey, //Payer
tokenATA, //Associated token account
payer.publicKey, //Token account owner
mintKeypair.publicKey, //Mint
),
4. createMintToInstruction - Instruction that will define what tokens to mint, where to mint them to, and how many to mint. We will pass mint (our mint public key), destination (the token account where the tokens should go, our tokenATA), authority (the authority that is able to mint new tokens for the mint--the same mintAuthority we defined in CreateInitializeMintInstruction above), and amount (number of tokens to mint--note that like lamports, we must pass the decimal value of our desired token amount by using Math.pow(10) and our MINT_CONFIG).
createMintToInstruction(
mintKeypair.publicKey, //Mint
tokenATA, //Destination Token Account
mintAuthority, //Authority
MINT_CONFIG.numberTokens * Math.pow(10, MINT_CONFIG.numDecimals),//number of tokens
),
5. createCreateMetadataAccountV3Instruction - associate our token meta data with this mint. We will pass CreateMetadataAccountV3InstructionAccounts, an object that includes metadata (the program derived address, metadataPDA that we've already defined), mint (public key of our mint), mintAuthority (the same authority set in createInitializeMintInstruction), payer (entity to cover transaction fees), updateAuthority (we'll use the same mintAuthority from our parameters). Then we must pass createMetadataAccountArgsV3 which includes data (this is the actual data that will be stored on chain, our ON_CHAIN_METADATA) and isMutable a boolean that sets whether or not you can change this metadata in the future.
createCreateMetadataAccountV3Instruction({
metadata: metadataPDA,
mint: mintKeypair.publicKey,
mintAuthority: mintAuthority,
payer: payer.publicKey,
updateAuthority: mintAuthority,
}, {
createMetadataAccountArgsV3: {
data: ON_CHAIN_METADATA,
isMutable: true,
collectionDetails: null
}
})
Whew! That's a big transaction! After we create the transaction, we'll want our function to return it. Add a return to your transaction:
return createNewTokenTransaction;
Your final function should look something like this:
const createNewMintTransaction = async (connection:Connection, payer:Keypair, mintKeypair: Keypair, destinationWallet: PublicKey, mintAuthority: PublicKey, freezeAuthority: PublicKey)=>{
//Get the minimum lamport balance to create a new account and avoid rent payments
const requiredBalance = await getMinimumBalanceForRentExemptMint(connection);
//metadata account associated with mint
const metadataPDA = await metaplex.nfts().pdas().metadata({ mint: mintKeypair.publicKey });
//get associated token account of your wallet
const tokenATA = await getAssociatedTokenAddress(mintKeypair.publicKey, destinationWallet);
const createNewTokenTransaction = new Transaction().add(
SystemProgram.createAccount({
fromPubkey: payer.publicKey,
newAccountPubkey: mintKeypair.publicKey,
space: MINT_SIZE,
lamports: requiredBalance,
programId: TOKEN_PROGRAM_ID,
}),
createInitializeMintInstruction(
mintKeypair.publicKey, //Mint Address
MINT_CONFIG.numDecimals, //Number of Decimals of New mint
mintAuthority, //Mint Authority
freezeAuthority, //Freeze Authority
TOKEN_PROGRAM_ID),
createAssociatedTokenAccountInstruction(
payer.publicKey, //Payer
tokenATA, //Associated token account
payer.publicKey, //token owner
mintKeypair.publicKey, //Mint
),
createMintToInstruction(
mintKeypair.publicKey, //Mint
tokenATA, //Destination Token Account
mintAuthority, //Authority
MINT_CONFIG.numberTokens * Math.pow(10, MINT_CONFIG.numDecimals),//number of tokens
),
createCreateMetadataAccountV3Instruction({
metadata: metadataPDA,
mint: mintKeypair.publicKey,
mintAuthority: mintAuthority,
payer: payer.publicKey,
updateAuthority: mintAuthority,
}, {
createMetadataAccountArgsV3: {
data: ON_CHAIN_METADATA,
isMutable: true,
collectionDetails: null
}
})
);
return createNewTokenTransaction;
}
Pull it All Together
Alright, you're almost done. We just need a function that pulls all of this together that we can execute in our app. Create a new async function called main with the following code:
const main = async() => {
console.log(`---STEP 1: Uploading MetaData---`);
const userWallet = Keypair.fromSecretKey(new Uint8Array(secret));
let metadataUri = await uploadMetadata(MY_TOKEN_METADATA);
ON_CHAIN_METADATA.uri = metadataUri;
console.log(`---STEP 2: Creating Mint Transaction---`);
let mintKeypair = Keypair.generate();
console.log(`New Mint Address: `, mintKeypair.publicKey.toString());
const newMintTransaction:Transaction = await createNewMintTransaction(
solanaConnection,
userWallet,
mintKeypair,
userWallet.publicKey,
userWallet.publicKey,
userWallet.publicKey
);
console.log(`---STEP 3: Executing Mint Transaction---`);
let { lastValidBlockHeight, blockhash } = await solanaConnection.getLatestBlockhash('finalized');
newMintTransaction.recentBlockhash = blockhash;
newMintTransaction.lastValidBlockHeight = lastValidBlockHeight;
newMintTransaction.feePayer = userWallet.publicKey;
const transactionId = await sendAndConfirmTransaction(solanaConnection,newMintTransaction,[userWallet,mintKeypair]);
console.log(`Transaction ID: `, transactionId);
console.log(`Succesfully minted ${MINT_CONFIG.numberTokens} ${ON_CHAIN_METADATA.symbol} to ${userWallet.publicKey.toString()}.`);
console.log(`View Transaction: https://explorer.solana.com/tx/${transactionId}?cluster=devnet`);
console.log(`View Token Mint: https://explorer.solana.com/address/${mintKeypair.publicKey.toString()}?cluster=devnet`)
}
You'll see our main function has 3 steps:
1. Upload Metadata. We start by loading our secret from the .json we created earlier in this exercise to define a userWallet Keypair. We use that wallet and pass MY_TOKEN_METADATA into Metaplex's uploadMetadata method to return an Arweave URI storing our metadata. We then set ON_CHAIN_METADATA.uri to that URI.
2. Create our mint transaction. Start by creating a new keypair for our new mint account. Then, run our CreateNewMintTransaction function, passing in our solanaConnection, userWallet, mintKeypair, and userWallet.publicKey.
3. Finally, execute the transaction. Pass newMintTransaction into sendAndConfirmTransaction with userWallet and mintKeypair as signers. Log our results!
Run Your Code
Finally, call main on the final line of your code:
main();
We've made our final code available here.
In your Terminal, type:
ts-node mint.ts
You should see step progression for each of our three steps and a final URL to the successful transaction:
If you click the "View Token Mint" link in your terminal, you should be able to see your new Token on Solana Explorer! Here's ours:
Note: If you've minted a fungible token in the past, you've probably submitted your token to the Solana Token Program Registry. That registry is now deprecated and no longer a necessary step. You've already uploaded the metadata on chain, so you're good to go!
Party Time! 🎉
Congrats! You just minted your own token on Solana using the new Metaplex fungible token standard. Have some fun with this--we're excited to see what tokens you're creating! To learn more, check out some of our other Solana tutorials here.
We ❤️ Feedback!
If you have any feedback or questions on this guide, let us know. Or, feel free to reach out to us via Twitter or our Discord community server. We’d love to hear from you!