Intermediate

 

Token program on the Solana blockchain using Javascript

This tutorial introduces the Geyser plugin for Solana validators. The plugin mechanism enhances the validator’s performance under heavy RPC loads by transmitting information about accounts, slots, blocks, and transactions to external data stores such as relational databases or NoSQL databases. This allows the validator to focus on processing transactions without being slowed down by busy RPC requests. The tutorial covers the plugin interface defined in the solana-geyser-plugin-interface crate, the referential plugin implementation for PostgreSQL database in the solana-accountsdb-plugin-postgres crate, and the configuration file for the plugin. The tutorial also provides information on how to implement the plugin and use it in the validator.

This program defines a common implementation for Fungible and Non Fungible tokens.

Source

The Token Program’s source is available on github

Interface

The Token Program is written in Rust and available on crates.io and docs.rs.

Auto-generated C bindings are also available here

JavaScript bindings are available that support loading the Token Program on to a chain and issue instructions.

See the SPL Associated Token Account program for convention around wallet address to token account mapping and funding.

Reference Guide

Setup

  • JS

Yarn

yarn add @solana/spl-token

 

npm

npm install @solana/spl-token

 

Configuration

You can connect to different clusters using Connection in @solana/web3.js

const web3 = require('@solana/web3.js');
const connection = new web3.Connection(web3.clusterApiUrl('devnet'), 'confirmed');

 

Keypair

You can either get your keypair using Keypair from @solana/web3.js, or let the user’s wallet handle the keypair and use sendTransaction from wallet-adapter

Airdrop SOL

Creating tokens and accounts requires SOL for account rent deposits and transaction fees. If the cluster you are targeting offers a faucet, you can get a little SOL for testing:

  • JS

import { clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js';

const payer = Keypair.generate();

const connection = new Connection(
  clusterApiUrl('devnet'),
  'confirmed'
);

const airdropSignature = await connection.requestAirdrop(
  payer.publicKey,
  LAMPORTS_PER_SOL,
);

await connection.confirmTransaction(airdropSignature);

 

Example: Creating your own fungible token

JS

import { createMint } from '@solana/spl-token';
import { clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js';

const payer = Keypair.generate();
const mintAuthority = Keypair.generate();
const freezeAuthority = Keypair.generate();

const connection = new Connection(
  clusterApiUrl('devnet'),
  'confirmed'
);

const mint = await createMint(
  connection,
  payer,
  mintAuthority.publicKey,
  freezeAuthority.publicKey,
  9 // We are using 9 to match the CLI decimal default exactly
);

console.log(mint.toBase58());
// AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM

 

The unique identifier of the token is AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM.

Tokens when initially created by spl-token have no supply:

JS

const mintInfo = await getMint(
  connection,
  mint
)

console.log(mintInfo.supply);
// 0

 

Let’s mint some. First create an account to hold a balance of the new AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM token:

JS

const tokenAccount = await getOrCreateAssociatedTokenAccount(
  connection,
  payer,
  mint,
  payer.publicKey
)

console.log(tokenAccount.address.toBase58());
// 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi

 

7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi is now an empty account:

JS

const tokenAccountInfo = await getAccount(
  connection,
  tokenAccount.address
)

console.log(tokenAccountInfo.amount);
// 0

 

Mint 100 tokens into the account:

JS

await mintTo(
  connection,
  payer,
  mint,
  tokenAccount.address,
  mintAuthority,
  100000000000 // because decimals for the mint are set to 9 
)

The token supply and account balance now reflect the result of minting:

JS

const mintInfo = await getMint(
  connection,
  mint
)

console.log(mintInfo.supply);
// 100

const tokenAccountInfo = await getAccount(
  connection,
  tokenAccount.address
)

console.log(tokenAccountInfo.amount);
// 100

 

Example: View all Tokens that you own

JS

import {AccountLayout, TOKEN_PROGRAM_ID} from "@solana/spl-token";
import {clusterApiUrl, Connection, PublicKey} from "@solana/web3.js";

(async () => {

  const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');

  const tokenAccounts = await connection.getTokenAccountsByOwner(
    new PublicKey('8YLKoCu7NwqHNS8GzuvA2ibsvLrsg22YMfMDafxh1B15'),
    {
      programId: TOKEN_PROGRAM_ID,
    }
  );

  console.log("Token                                         Balance");
  console.log("------------------------------------------------------------");
  tokenAccounts.value.forEach((tokenAccount) => {
    const accountData = AccountLayout.decode(tokenAccount.account.data);
    console.log(`${new PublicKey(accountData.mint)}   ${accountData.amount}`);
  })

})();

/*
Token                                         Balance
------------------------------------------------------------
7e2X5oeAAJyUTi4PfSGXFLGhyPw2H8oELm1mx87ZCgwF  84
AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM  100
AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM  0
AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM  1
*/

 

Example: Wrapping SOL in a Token

When you want to wrap SOL, you can send SOL to an associated token account on the native mint and call syncNative. syncNative updates the amount field on the token account to match the amount of wrapped SOL available. That SOL is only retrievable by closing the token account and choosing the desired address to send the token account’s lamports.

JS

import {NATIVE_MINT, createAssociatedTokenAccountInstruction, getAssociatedTokenAddress, createSyncNativeInstruction, getAccount} from "@solana/spl-token";
import {clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL, SystemProgram, Transaction, sendAndConfirmTransaction} from "@solana/web3.js";

(async () => {

const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');

const wallet = Keypair.generate();

const airdropSignature = await connection.requestAirdrop(
  wallet.publicKey,
  2 * LAMPORTS_PER_SOL,
);

await connection.confirmTransaction(airdropSignature);

const associatedTokenAccount = await getAssociatedTokenAddress(
  NATIVE_MINT,
  wallet.publicKey
)

// Create token account to hold your wrapped SOL
const ataTransaction = new Transaction()
  .add(
    createAssociatedTokenAccountInstruction(
      wallet.publicKey,
      associatedTokenAccount,
      wallet.publicKey,
      NATIVE_MINT
    )
  );

await sendAndConfirmTransaction(connection, ataTransaction, [wallet]);

// Transfer SOL to associated token account and use SyncNative to update wrapped SOL balance
const solTransferTransaction = new Transaction()
  .add(
    SystemProgram.transfer({
        fromPubkey: wallet.publicKey,
        toPubkey: associatedTokenAccount,
        lamports: LAMPORTS_PER_SOL
      }),
      createSyncNativeInstruction(
        associatedTokenAccount
    )
  )

await sendAndConfirmTransaction(connection, solTransferTransaction, [wallet]);

const accountInfo = await getAccount(connection, associatedTokenAccount);

console.log(`Native: ${accountInfo.isNative}, Lamports: ${accountInfo.amount}`);

})();

To unwrap the Token back to SOL:

JS

const walletBalance = await connection.getBalance(wallet.publicKey);

console.log(`Balance before unwrapping 1 WSOL: ${walletBalance}`)

await closeAccount(connection, wallet, associatedTokenAccount, wallet.publicKey, wallet);

const walletBalancePostClose = await connection.getBalance(wallet.publicKey);

console.log(`Balance after unwrapping 1 WSOL: ${walletBalancePostClose}`)

/*
Balance before unwrapping 1 WSOL: 997950720
Balance after unwrapping 1 WSOL: 1999985000
*/

 

Note: Some lamports were removed for transaction fees

Example: Transferring tokens to another user

First the receiver uses spl-token create-account to create their associated token account for the Token type. Then the receiver obtains their wallet address by running solana address and provides it to the sender.

The sender then runs:

JS

import { clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL } from '@solana/web3.js';
import { createMint, getOrCreateAssociatedTokenAccount, mintTo, transfer } from '@solana/spl-token';

(async () => {
    // Connect to cluster
    const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');

    // Generate a new wallet keypair and airdrop SOL
    const fromWallet = Keypair.generate();
    const fromAirdropSignature = await connection.requestAirdrop(fromWallet.publicKey, LAMPORTS_PER_SOL);

    // Wait for airdrop confirmation
    await connection.confirmTransaction(fromAirdropSignature);

    // Generate a new wallet to receive newly minted token
    const toWallet = Keypair.generate();

    // Create new token mint
    const mint = await createMint(connection, fromWallet, fromWallet.publicKey, null, 9);

    // Get the token account of the fromWallet address, and if it does not exist, create it
    const fromTokenAccount = await getOrCreateAssociatedTokenAccount(
        connection,
        fromWallet,
        mint,
        fromWallet.publicKey
    );

    // Get the token account of the toWallet address, and if it does not exist, create it
    const toTokenAccount = await getOrCreateAssociatedTokenAccount(connection, fromWallet, mint, toWallet.publicKey);

    // Mint 1 new token to the "fromTokenAccount" account we just created
    let signature = await mintTo(
        connection,
        fromWallet,
        mint,
        fromTokenAccount.address,
        fromWallet.publicKey,
        1000000000
    );
    console.log('mint tx:', signature);

    // Transfer the new token to the "toTokenAccount" we just created
    signature = await transfer(
        connection,
        fromWallet,
        fromTokenAccount.address,
        toTokenAccount.address,
        fromWallet.publicKey,
        50
    );
})();

 

Example: Transferring tokens to another user, with sender-funding

If the receiver does not yet have an associated token account, the sender may choose to fund the receiver’s account.

The receiver obtains their wallet address by running solana address and provides it to the sender.

The sender then runs to fund the receiver’s associated token account, at the sender’s expense, and then transfers 50 tokens into it:

JS

const signature = await transfer(
    connection,
    toWallet,
    fromTokenAccount.address,
    toTokenAccount.address,
    fromWallet.publicKey,
    50,
    [fromWallet, toWallet]
);

 

Example: Transferring tokens to an explicit recipient token account

Tokens may be transferred to a specific recipient token account. The recipient token account must already exist and be of the same Token type.

JS

import {getAccount, createMint, createAccount, mintTo, getOrCreateAssociatedTokenAccount, transfer} from "@solana/spl-token";
import {clusterApiUrl, Connection, Keypair, LAMPORTS_PER_SOL} from "@solana/web3.js";

(async () => {

  const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');

  const wallet = Keypair.generate();
  const auxiliaryKeypair = Keypair.generate();

  const airdropSignature = await connection.requestAirdrop(
    wallet.publicKey,
    LAMPORTS_PER_SOL,
  );

  await connection.confirmTransaction(airdropSignature);

  const mint = await createMint(
    connection,
    wallet,
    wallet.publicKey,
    wallet.publicKey,
    9
  );

  // Create custom token account
  const auxiliaryTokenAccount = await createAccount(
    connection,
    wallet,
    mint,
    wallet.publicKey,
    auxiliaryKeypair
  );

  const associatedTokenAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    wallet,
    mint,
    wallet.publicKey
  );

  await mintTo(
    connection,
    wallet,
    mint,
    associatedTokenAccount.address,
    wallet,
    50
  );

  const accountInfo = await getAccount(connection, associatedTokenAccount.address);

  console.log(accountInfo.amount);
  // 50

  await transfer(
    connection,
    wallet,
    associatedTokenAccount.address,
    auxiliaryTokenAccount,
    wallet,
    50
  );

  const auxAccountInfo = await getAccount(connection, auxiliaryTokenAccount);

  console.log(auxAccountInfo.amount);
  // 50
})();

 

Example: Create a non-fungible token

Create the token type with zero decimal place,

JS

const mint = await createMint(
  connection,
  wallet,
  wallet.publicKey,
  wallet.publicKey,
  0
);

 

then create an account to hold tokens of this new type:

JS

const associatedTokenAccount = await getOrCreateAssociatedTokenAccount(
    connection,
    wallet,
    mint,
    wallet.publicKey
  );

 

 

Now mint only one token into the account,

JS

await mintTo(
    connection,
    wallet,
    mint,
    associatedTokenAccount.address,
    wallet,
    1
  );

 

and disable future minting:

JS

let transaction = new Transaction()
  .add(createSetAuthorityInstruction(
    mint,
    wallet.publicKey,
    AuthorityType.MintTokens,
    null
  ));

await web3.sendAndConfirmTransaction(connection, transaction, [wallet]);

 

Now the 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM account holds the one and only 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z token:

JS

const accountInfo = await getAccount(connection, associatedTokenAccount.address);

console.log(accountInfo.amount);
// 1
const mintInfo = await getMint(
    connection,
    mint
  );

console.log(mintInfo);
/*
{
  address: "7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM",
  mintAuthority: "559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z",
  supply: 1,
  decimals: 0,
  isInitialized: true,
  freezeAuthority: "vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg"
}
 */

 

Multisig usage

JS

The main difference in using multisign is specifying the owner as the multisig key, and giving the list of signers when contructing a transaction. Normally you would provide the signer that has authority to run the transaction as the owner, but in the multisig case the owner would be the multisig key.

Multisig accounts can be used for any authority on an SPL Token mint or token account.

  • Mint account mint authority:createMint(/* ... */, mintAuthority: multisigKey, /* ... */)

  • Mint account freeze authority:createMint(/* ... */, freezeAuthority: multisigKey, /* ... */)

  • Token account owner authority:getOrCreateAssociatedTokenAccount(/* ... */, mintAuthority: multisigKey, /* ... */)

  • Token account close authority:closeAccount(/* ... */, authority: multisigKey, /* ... */)

Example: Mint with multisig authority

First create keypairs to act as the multisig signer-set. In reality, these can be any supported signer, like: a Ledger hardware wallet, a keypair file, or a paper wallet. For convenience, generated keypairs will be used in this example.

JS

const signer1 = Keypair.generate();
const signer2 = Keypair.generate();
const signer3 = Keypair.generate();

In order to create the multisig account, the public keys of the signer-set must be collected.

JS

console.log(signer1.publicKey.toBase58());
console.log(signer2.publicKey.toBase58());
console.log(signer3.publicKey.toBase58());
/*
  BzWpkuRrwXHq4SSSFHa8FJf6DRQy4TaeoXnkA89vTgHZ
  DhkUfKgfZ8CF6PAGKwdABRL1VqkeNrTSRx8LZfpPFVNY
  D7ssXHrZJjfpZXsmDf8RwfPxe1BMMMmP1CtmX3WojPmG
 */

Now the multisig account can be created with the spl-token create-multisig subcommand. Its first positional argument is the minimum number of signers (M) that must sign a transaction affecting a token/mint account that is controlled by this multisig account. The remaining positional arguments are the public keys of all keypairs allowed (N) to sign for the multisig account. This example will use a “2 of 3” multisig account. That is, two of the three allowed keypairs must sign all transactions.

NOTE: SPL Token Multisig accounts are limited to a signer-set of eleven signers (1 <= N <= 11) and minimum signers must be no more than N (1 <= M <= N)

JS

const multisigKey = await createMultisig(
  connection,
  payer,
  [
    signer1.publicKey,
    signer2.publicKey,
    signer3.publicKey
  ],
  2
);

console.log(`Created 2/3 multisig ${multisigKey.toBase58()}`);
// Created 2/3 multisig 46ed77fd4WTN144q62BwjU2B3ogX3Xmmc8PT5Z3Xc2re

Next create the token mint and receiving accounts as previously described and set the mint account’s minting authority to the multisig account

JS

const mint = await createMint(
    connection,
    payer,
    multisigKey,
    multisigKey,
    9
  );

const associatedTokenAccount = await getOrCreateAssociatedTokenAccount(
  connection,
  payer,
  mint,
  signer1.publicKey
);

To demonstrate that the mint account is now under control of the multisig account, attempting to mint with one multisig signer fails

JS

try {
  await mintTo(
    connection,
    payer,
    mint,
    associatedTokenAccount.address,
    multisigKey,
    1
  )
} catch (error) {
  console.log(error);
}
// Error: Signature verification failed

But repeating with a second multisig signer, succeeds

JS

await mintTo(
  connection,
  payer,
  mint,
  associatedTokenAccount.address,
  multisigKey,
  1,
  [
    signer1,
    signer2
  ]
)

const mintInfo = await getMint(
  connection,
  mint
)

console.log(`Minted ${mintInfo.supply} token`);
// Minted 1 token

Example: Offline signing with multisig

Sometimes online signing is not possible or desireable. Such is the case for example when signers are not in the same geographic location or when they use air-gapped devices not connected to the network. In this case, we use offline signing which combines the previous examples of multisig with offline signing and a nonce account.

This example will use the same mint account, token account, multisig account, and multisig signer-set keypair filenames as the online example, as well as a nonce account that we create here:

JS

const connection = new Connection(
  clusterApiUrl('devnet'),
  'confirmed',
);

const onlineAccount = Keypair.generate();
const nonceAccount = Keypair.generate();

const minimumAmount = await connection.getMinimumBalanceForRentExemption(
  NONCE_ACCOUNT_LENGTH,
);

// Form CreateNonceAccount transaction
const transaction = new Transaction()
  .add(
  SystemProgram.createNonceAccount({
    fromPubkey: onlineAccount.publicKey,
    noncePubkey: nonceAccount.publicKey,
    authorizedPubkey: onlineAccount.publicKey,
    lamports: minimumAmount,
  }),
);

await web3.sendAndConfirmTransaction(connection, transaction, [onlineAccount, nonceAccount])

const nonceAccountData = await connection.getNonce(
  nonceAccount.publicKey,
  'confirmed',
);

console.log(nonceAccountData);
/*
NonceAccount {
  authorizedPubkey: '5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE'
  nonce: '6DPt2TfFBG7sR4Hqu16fbMXPj8ddHKkbU4Y3EEEWrC2E',
  feeCalculator: { lamportsPerSignature: 5000 }
}
 */

For the fee-payer and nonce-authority roles, a local hot wallet at 5hbZyJ3KRuFvdy5QBxvE9KwK17hzkAUkQHZTxPbiWffE will be used.

JS

First a raw transaction is built using the nonceAccountInformation and tokenAccount key. All signers of the transaction are noted as part of the raw transaction. This transaction will be handed to the signers later for signing.

const nonceAccountInfo = await connection.getAccountInfo(
  nonceAccount.publicKey,
  'confirmed'
);

const nonceAccountFromInfo = web3.NonceAccount.fromAccountData(nonceAccountInfo.data);

console.log(nonceAccountFromInfo);

const nonceInstruction = web3.SystemProgram.nonceAdvance({
  authorizedPubkey: onlineAccount.publicKey,
  noncePubkey: nonceAccount.publicKey
});

const nonce = nonceAccountFromInfo.nonce;

const mintToTransaction = new web3.Transaction({
  feePayer: onlineAccount.publicKey,
  nonceInfo: {nonce, nonceInstruction}
})
  .add(
    createMintToInstruction(
      mint,
      associatedTokenAccount.address,
      multisigkey,
      1,
      [
        signer1,
        onlineAccount
      ],
      TOKEN_PROGRAM_ID
    )
  );

Next each offline signer will take the transaction buffer and sign it with their corresponding key.

 
let mintToTransactionBuffer = mintToTransaction.serializeMessage();

let onlineSIgnature = nacl.sign.detached(mintToTransactionBuffer, onlineAccount.secretKey);
mintToTransaction.addSignature(onlineAccount.publicKey, onlineSIgnature);

// Handed to offline signer for signature
let offlineSignature = nacl.sign.detached(mintToTransactionBuffer, signer1.secretKey);
mintToTransaction.addSignature(signer1.publicKey, offlineSignature);

let rawMintToTransaction = mintToTransaction.serialize();

Finally, the hot wallet will take the transaction, serialize it, and broadcast it to the network.

 
// Send to online signer for broadcast to network
await web3.sendAndConfirmRawTransaction(connection, rawMintToTransaction);

 

JSON RPC methods

There is a rich set of JSON RPC methods available for use with SPL Token:

  • getTokenAccountBalance

  • getTokenAccountsByDelegate

  • getTokenAccountsByOwner

  • getTokenLargestAccounts

  • getTokenSupply

See for more details.

Additionally the versatile getProgramAccounts JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest.

Finding all token accounts for a specific mint

To find all token accounts for the TESTpKgj42ya3st2SQTKiANjTBmncQSCqLAZGcSPLGM mint:

curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      {
        "encoding": "jsonParsed",
        "filters": [
          {
            "dataSize": 165
          },
          {
            "memcmp": {
              "offset": 0,
              "bytes": "TESTpKgj42ya3st2SQTKiANjTBmncQSCqLAZGcSPLGM"
            }
          }
        ]
      }
    ]
  }
'

The "dataSize": 165 filter selects all Token Accounts, and then the "memcmp": ... filter selects based on the mint address within each token account.

Finding all token accounts for a wallet

Find all token accounts owned by the vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg user

curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" -d '
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getProgramAccounts",
    "params": [
      "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
      {
        "encoding": "jsonParsed",
        "filters": [
          {
            "dataSize": 165
          },
          {
            "memcmp": {
              "offset": 32,
              "bytes": "vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg"
            }
          }
        ]
      }
    ]
  }
'

he "dataSize": 165 filter selects all Token Accounts, and then the "memcmp": ... filter selects based on the owner address within each token account.