From Scratch: How to Create Your Own Token on Solana with Solana Cli and Typescript

From Scratch: How to Create Your Own Token on Solana with Solana Cli and Typescript

ยท

14 min read

Recently, I came across a video by Harkirat Singh where he gave a step-by-step guide to create your own custom token on the Solana blockchain, and at the end of the video, he gave some assignments to be completed. In this article, I am going to tell you how I completed those assignments and by the end of this article, you will have a basic understanding of how tokens and wallets work on the Solana blockchain. You will also be able to create your own Solana token and have basic knowledge about working with CLI and TypeScript on Solana.

Prerequisite:

  • Basic javascript knowledge to understand code

Takeaway:

  • Beginners will get hands-on experience with WEB3

  • Deep understanding of token transfer

So now we have set the context, let's get started.

One thing to keep in mind is that we will be doing all the steps in Solana Devnet. The Solana Devnet is an environment that replicates the actual Solana network (i.e. Mainnet) environment, where you can access all functions of the main net for testing and development purposes without actually interacting or risking real assets on the mainnet.

How token creation and transfer work in Solana network

  1. Solana network: It consists of multiple nodes that validate the transaction done on Solana network.

  2. Smart Contract: Smart Contracts are contracts that can be deployed on the Solana network. One of these Smart contracts is already deployed on the network that will help us in building our token. We will not go into much detail of smart contract as we are not going to create smart contract of our own instead we will be using already existing smart contract under the hood.

  3. Mint: Mint is a unique address referring to a particular token which can be used to identify that token. Each token will have its own mint address. From the diagram, you can see our custom token C1 has its own mint. For easy understanding, you can consider C1 as a 100 rupee note and Mint for C1 is a machine that can print only 100 rupee notes.

  4. Bank Wallet: Bank Wallet is the wallet address of the owner(person who created the token) of the token. For a real-world analogy, you can consider it as the government that owns the currency and can print it. Wallet can have multiple accounts under it for holding multiple types of tokens. More about it under the next few points.

  5. User Wallet: User wallet is the wallet address of the person to whom the token is to be sent. This can be considered as the wallet for general people like us.

  6. Account: Account is something that can hold a particular type of token and is related to the mint address of that token to uniquely identify it. So an account that is made to hold a C1 token will not be able to hold any other token. To hold the C2 token a new account needs to be created specifically for holding C2.

  7. Bank Account: Bank Account is the account that is the owner of the mint and can hold tokens specified by that mint. All the tokens created by that mint will go to this bank account initially and later it can be sent to where ever the bank wallet owner wants to send it to.

  8. User Account: User account is the account of the user to whom the token is being sent.

Note: If the token is being sent to any User's wallet address and that user is not having an account already created for that particular token then first the account needs to be created for that user so that the user can hold that token under that account.

Now that you have an understanding of how it works, let's get straight into the action and create our first Solana token.

Step 1: Creation of user wallet

First, let's create the user wallet for that we will use backpack. It is a chrome extension that can be added from here https://www.backpack.app/. For the invite code, you can use any of the ones provided in this sheet backpack invite codes.

While setting up the wallet select Solana network. It's not necessary to use this wallet you can use any wallet supporting Solana network.

Step 2: Setting up Solana tools in your local system

For setting up the Solana tools refer to this link. I am using mac I didn't face any issues. For Windows users I will suggest installing WSL(Windows Subsystem for Linux) and then installing Solana tools.

If you have successfully installed you can check the version using the below given command.

solana --version

Step 3: Creation of Bank Wallet

For creating bank wallet we are going to use the terminal. So open terminal/Solana CLI and enter this command:

solana-keygen new

It will create the wallet and show you the public key in the terminal. It will put the private key in /Users/username/.config/solana/id.json location in your system. It will show the location in the terminal as well. Also, it will show you passphrase in the terminal. Keep that passphrase somewhere safe. This passphrase can be used to recover your private key.

Image description

Step 4: Creating a custom token

As we discussed in starting we will be doing operation in devnet as it will not cost us anything so to configure devnet in your local run the below given command in your terminal.

solana config set --url https://api.devnet.solana.com

One more thing to take care, performing any operation in Solana network cost you some sol tokens called gas fees. As we just created our wallet we don't have any sol tokens and the balance is 0. You can verify that with the below command.

solana balance

As devnet is only for development purposes you can airdrop yourself some sol token without paying anything in actual. To airdrop 1 Sol to your wallet run the below command

solana airdrop 1
solana balance

Image description

Now that we have both user and bank wallet, and bank wallet is having sol to perform operations on solana network let's create our first token. For that run the following command

spl-token create-token

Image description

After running the above given command you can see that your token is created as shown in the image. It will give you the mintaddress of the token and decimal. By default, it will be 9. Decimal means up to what fraction level your token can be broken while transferring.

You can go to https://explorer.solana.com/ and search for your token address. It will start showing you your newly created token. Solana explorer allows you to look up transactions and accounts on Solana network. Few things to keep in mind when we refer to transactions in Solana network, it is not only the transfer of tokens instead every operation you perform is considered a transaction and has a transaction id attached to it which can be used to look into the operation performed. Also as you have created a token on devnet so don't forget to change the cluster on the top right corner to devnet in Solana explorer.

Currently, your token will be shown as an Unknown Token as we have not given any name to it.

Now your token is created but there is no supply for it. This means you need to mint(print) your token(money) to provide supply for it and also you will need a bank account to hold the minted token. So let's do that For that run the below commands:

spl-token create-account mintAddress

spl-token mint mintAddress 10000

Image description

Now 10000 custom token has been minted and are present in your bank account from where they can be sent to any other wallet to bring them into circulation.

On solana explorer it will look something like this

Image description

Now Let's tranfer some tokens to user wallet and see what happens. To transfer the token run the below given command

spl-token transfer mintAddress 3000 userWalletAddress --allow-unfunded-recipient --fund-recipient

Here as the user wallet is not having the account to hold this token and this token is first time being sent to the user wallet we need to add --allow-unfunded-recipient --fund-recipient in command.

Image description

If you go and check in Solana explorer through your user wallet public key you will be able to see these tokens under the user wallet but directly under your backpack user wallet interface under devnet cluster, these will not be visible. The reason for this is that we have not attached metadata to it like name, image etc. Once we attach the metadata it will start showing in the wallet interface also.

Step 5: Updating metadata for token

Previously token metadata was stored in a GitHub repository but recently Solana has adopted Metaplex's Fungible Token Standard. Using the standard with your token mint will enable platforms like backpack, Phantom wallet, Solana Explorer and others to easily recognize your token and make it viewable by their users.

To attach metadata to a token we will do it through a typescript program but before moving to code let's understand why we attach metadata separately and how it is stored.

Metadata files are generally small in size but still, it is neither economically nor technically sound to store them on the blockchain. That's why decentralized storage solutions typically host metadata files, and IPFS remains one of the most popular decentralized storage protocols. So the image can be put on some hosting site and its URL can be referred to in metadata json. This metadata json should also be kept on some decentralized storage solution for example arweave and this arweave url can be kept provided in metadata account which is attached to the mint. Here is a sample arweave url having this data. We will see all of this happening in code as well.
I will also recommend you to go through the introduction and json standard section of this doc to get a basic understanding of token-metadata.

So now once we have an understanding of what we want to do, let's look into the code.
First, upload your token image to any decentralized storage solution and get the public URL. This URL we will provide in metadata. I used the web3 image host

Then we will create a connection with the Solana network in devnet cluster.

const endpoint = 'https://api.devnet.solana.com/'; 
const solanaConnection = new Connection(endpoint);

Next, we will create the metadata objects which we will be uploading to arweave. For the image provide the URL that we created above.

const MY_TOKEN_METADATA: UploadMetadataInput = {
    name: "HippoCoin",
    symbol: "HIPP0",
    description: "This is a hippo token!",
    image: "https://ipfs.io/ipfs/bafybeidljnhkf3oaoyux7gubcl6glv5sptqvbmqpg2dgmk7lkliai5g7de?filename=DALL%C2%B7E%202023-03-16%2017.26.56%20-%20Cute%20cartoon%20hippo%20looking%20in%20front.png" 
}

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;

Next, this method will help us in uploading the metadata json and getting the arweave URL. It takes bank wallet KeyPair and token metadata object as parameters. We are using metaplex library to upload this.

const uploadMetadata = async(keypair: Keypair, tokenMetadata: UploadMetadataInput):Promise => {
    //create metaplex instance on devnet using this wallet
    const metaplex = Metaplex.make(solanaConnection)
        .use(keypairIdentity(keypair))
        .use(bundlrStorage({
        address: 'https://devnet.bundlr.network',
        providerUrl: endpoint,
        timeout: 60000,
        }));

    //Upload to Arweave
    const { uri } = await metaplex.nfts().uploadMetadata(tokenMetadata);
    console.log(`Arweave URL: `, uri);
    return uri;
}

Now we will create another method that will create a transaction to attach this to your token. In this method also we require two parameter i.e PublicKey of mint with which we want to attach metadata and mintAuthority which is the PublicKey of the bank wallet.

Note that in this method we are using createCreateMetadataAccountV2Instruction method that is used to attach the metadata account first time to the mint. Once the metadata account is attached you can keep updating the metadata account values using createUpdateMetadataAccountV2Instruction method.

const addMetaDataTransaction = async (mintKeypair: PublicKey, mintAuthority: PublicKey)=>{
    const metadataPDA = await findMetadataPda(mintKeypair);

    const addMetaDataTransaction = new Transaction().add(
        createCreateMetadataAccountV2Instruction({
            metadata: metadataPDA, 
            mint: mintKeypair, 
            mintAuthority: mintAuthority,
            payer: mintAuthority,
            updateAuthority: mintAuthority,
          },
          { createMetadataAccountArgsV2: 
            { 
              data: ON_CHAIN_METADATA, 
              isMutable: true 
            } 
          }
        )
    );


    return addMetaDataTransaction;
}

Now that we have all the things in place let's create the main method by which we will execute this transaction.

const main = async() => {
    console.log(`---STEP 1: Uploading MetaData---`);
    const bankWalletKeyPair = Keypair.fromSecretKey(new Uint8Array(secret));
    let metadataUri = await uploadMetadata(bankWalletKeyPair, MY_TOKEN_METADATA);
    ON_CHAIN_METADATA.uri = metadataUri;

    console.log(`---STEP 2: Creating Transaction---`);

    const MINT_ADDRESS = "9e5H7DWECb69zDifMHAnx2R7r1SWMAa3xHF9NYQr76gM";
    const mintKeypair = new PublicKey(MINT_ADDRESS);
    const addMetaDataTx:Transaction = await addMetaDataTransaction(
        mintKeypair,
        bankWalletKeyPair.publicKey
    );

    console.log(`---STEP 3: Executing Transaction---`);
    const transactionId =  await solanaConnection.sendTransaction(addMetaDataTx, [bankWalletKeyPair,bankWalletKeyPair]);
    console.log(`Transaction ID: `, transactionId);
    console.log(`View Transaction: https://explorer.solana.com/tx/${transactionId}?cluster=devnet`);
}

main();

To create bankWalletKeyPair in the above code we are using the private key of our wallet from the JSON file present in /Users/username/.config/solana/id.json that came after we ran solana-keygen new command in the terminal while creating bank wallet.
Below are the imports. Here we have used Solana and metaplex libraries.

import { Transaction, Keypair, Connection, PublicKey } from "@solana/web3.js";
import { bundlrStorage, findMetadataPda, keypairIdentity, Metaplex, UploadMetadataInput } from '@metaplex-foundation/js';
import { DataV2, createCreateMetadataAccountV2Instruction,createUpdateMetadataAccountV2Instruction } from '@metaplex-foundation/mpl-token-metadata';
import secret from './walletSecret.json';

To run this file you can give this command.

ts-node filename

Image description

Now that you have added metadata to your token, it will start displaying the metadata details like name and image in Solana explorer. Also, it will start showing in your user wallet now.

Image description

Step 6: Transferring the token programmatically

In step 4 we have already seen how we can transfer tokens using terminal/CLI so now let's transfer tokens using code.

First, we will import the below things from the library and also define the mint address and KeyPair of the source wallet from which the token is to be sent. walletSecret.json will contain the private key of the wallet.

import { getOrCreateAssociatedTokenAccount, createTransferInstruction } from "@solana/spl-token";
import { Connection, Keypair, ParsedAccountData, PublicKey, sendAndConfirmTransaction, Transaction } from "@solana/web3.js";
import secret from './walletSecret.json';

const FROM_KEYPAIR = Keypair.fromSecretKey(new Uint8Array(secret));
const MINT_ADDRESS = 'BQi3xCLLJferBFX9vr1g68cV8nKpBGLJ6825izTkNUF2';

Then we will create the connection with the solana network on devnet

/**
 * Creating connection with solana network
 */
const endpoint = 'https://api.devnet.solana.com/';
const solanaConnection = new Connection(endpoint);

Next, we will have a method that will fetch the numberOfDecimals defined for our token or mint. This will be useful for sending the token as a whole number. While specifying the numberOfTokens to be sent in createTransferInstruction method we will have to multiply the numberOfTokens with 10*numberOfDecimals otherwise fractional part of the token will be sent.

async function getNumberDecimals(mintAddress: string):Promise {
    const info = await solanaConnection.getParsedAccountInfo(new PublicKey(MINT_ADDRESS));
    const result = (info.value?.data as ParsedAccountData).parsed.info.decimals as number;
    return result;
}

Now finally we will have sendTokens method that will take the destination wallet address and the transfer amount as parameters.

async function sendTokens(destination_Wallet: String, transfer_Amount: number) {
    console.log(`Sending ${transfer_Amount} ${(MINT_ADDRESS)} from ${(FROM_KEYPAIR.publicKey.toString())} to ${(destination_Wallet)}.`)
    //Step 1
    console.log(`1 - Getting Source Token Account`);
    let sourceAccount = await getOrCreateAssociatedTokenAccount(
        solanaConnection, 
        FROM_KEYPAIR,
        new PublicKey(MINT_ADDRESS),
        FROM_KEYPAIR.publicKey
    );
    console.log(`    Source Account: ${sourceAccount.address.toString()}`);

        //Step 2
        console.log(`2 - Getting Destination Token Account`);
        let destinationAccount = await getOrCreateAssociatedTokenAccount(
            solanaConnection, 
            FROM_KEYPAIR,
            new PublicKey(MINT_ADDRESS),
            new PublicKey(destination_Wallet)
        );
        console.log(`    Destination Account: ${destinationAccount.address.toString()}`);

            //Step 3
    console.log(`3 - Fetching Number of Decimals for Mint: ${MINT_ADDRESS}`);
    const numberDecimals = await getNumberDecimals(MINT_ADDRESS);
    console.log(`    Number of Decimals: ${numberDecimals}`);

        //Step 4
        console.log(`4 - Creating and Sending Transaction`);
        const tx = new Transaction();
        tx.add(createTransferInstruction(
            sourceAccount.address,
            destinationAccount.address,
            FROM_KEYPAIR.publicKey,
            transfer_Amount * Math.pow(10, numberDecimals)
        ))

        const latestBlockHash = await solanaConnection.getLatestBlockhash('confirmed');
        tx.recentBlockhash = await latestBlockHash.blockhash;    
        const signature = await sendAndConfirmTransaction(solanaConnection,tx,[FROM_KEYPAIR]);
        console.log(
            '\x1b[32m', //Green Text
            `   Transaction Success!๐ŸŽ‰`,
            `\n    https://explorer.solana.com/tx/${signature}?cluster=devnet`
        );

}

sendTokens("9e5H7DWECb69zDifMHAnx2R7r1SWMAa3xHF9NYQr76gM", 10);

In the above code notice getOrCreateAssociatedTokenAccount method. We already discussed before in case the destination wallet is not having the account to hold the token, then a new account needs to be created under that wallet for that token or mint so that it can hold the token. So getOrCreateAssociatedTokenAccount will fetch the existing token account if it exists for that wallet, in case it is not there it will create a new one.

createTransferInstruction method is used to create instruction to transfer the tokens and is added to the transaction.
Also, we are fetching recent blockhash before sending the transaction.

To run this file you can give this command

ts-node filename

Image description

You can find the code here.

Step 7: Limiting the supply of token

Now that we have minted the tokens and started transferring them, let's limit their supply so that no more tokens can be minted. For limiting their supply we need to disable the mint authority of that token.

One thing to keep in mind, once you have disabled the mint authority, you will no longer be able to update the metadata of the token.

To disable the mint authority we will run the below given command in terminal/CLI.

spl-token authorize 9e5H7DWECb69zDifMHAnx2R7r1SWMAa3xHF9NYQr76gM mint --disable

Now we no longer will be able to mint more of this token and the supply for this token is fixed.

You can verify this from Solana explorer as well. Previous to disabling the mint authority it would have shown you the token like in the below image.

After disabling the mint authority it will show you the token with Fixed Supply.

Conclusion

To conclude, in this article we understood how wallets, account and token transfer works. We created a wallet and a custom Solana token through the terminal/CLI. Then we attached metadata(name, image etc.) to that token using typescript code and then transferred it to another wallet address using typescript code. Finally, we fixed its supply by disabling the mintAuthority so that no more tokens can be minted.

Here is a list of wallets provided by quicknode that you can use for transferring your tokens to different wallet addresses: test wallets.

Github code here.

References:

  1. https://www.youtube.com/watch?v=8NeZgmSfbYg&t=2436s

  2. https://www.quicknode.com/guides/solana-development/spl-tokens/how-to-transfer-spl-tokens-on-solana/

ย