Solana development with Go
Do not index
Do not index
By the time I writing this article (Sep 4, 2024), there are mainly two Golang library for Solana:
Name
Star
Since
Remark
368
2 years ago
standalone documentation
804
4 years ago
Because I like solana-go-sdk’s documentation, so I start with it.
 

RPC methods

Account

Solana private key and public key looks like:
5P4WSVvi9VZJ9De8L1Msuei6jUmaYuJpuWDi4RMkGi6cme9hAeMhcBEYNMmJNmeXDQ3XxjYn6S8xJZir1bMvRzMN # 88 length of base58 char
F4rYYhhiK7RtG8MrkZ4nBfF2vhccm3gdEJgX3cKjQHvx # 44 length of base58 char
  • Public Key: The public key is 32 bytes (256 bits) long. It is often represented in Base58 format, which is typically 44 characters long. It’s be so called Solana Account.
  • Private Key: The private key is 64 bytes (512 bits) long, 88 characters long in Base58, which is a concatenation of:
    • The 32-byte private scalar.
    • The 32-byte public key (derived from the private scalar).
So if you leak your first half of your private key, you actually leak your whole key.

What is base58?

Base58 encoding was first invented by Satoshi Nakamoto, and it’s used by encoding bitcoin address. Solana also used it for encoding addresses, keys, and other data in a human-readable format.
Base58 uses a set of 58 characters, which includes:
  • Digits: 1-9 (excludes 0)
  • Uppercase letters: A-Z (excludes O)
  • Lowercase letters: a-z (excludes l and I)

Account length

Base58 represents data in 58 possible characters, which requires about 5.857 bits per character (log2(58) ≈ 5.857 bits), so 32 bytes (256 bits) of data, when encoded using Base58, yields a string of 32 bytes * 8 bits / 5.857 ≈ 44 characters.

Default account creation

When you create a new account on Solana, it is by default assigned to the System Program (11111111111111111111111111111111). This program is responsible for creating and managing accounts. So, until an account is assigned to a different program by a transaction or a specific instruction, it remains under the control of the System Program. The System Program allows for basic account operations such as creating accounts, transferring SOL, allocating space, etc.
 
Create a new account:
package main

import (
	"crypto/ed25519"
	"fmt"
	"github.com/blocto/solana-go-sdk/pkg/hdwallet"
	"github.com/blocto/solana-go-sdk/types"
	"github.com/mr-tron/base58"
	"github.com/tyler-smith/go-bip39"
)

func main() {
	// create a new account
	{
		account := types.NewAccount()
		fmt.Println("Extended Private Key:", base58.Encode(account.PrivateKey))

		// Extract the first 32 bytes (private scalar) from the full private key
		privateScalar := account.PrivateKey[:32]

		// Extract the last 32 bytes (public key) from the full private key
		extractedPublicKey := account.PrivateKey[32:]
		fmt.Println("Extracted Public Key from Private Key:", base58.Encode(extractedPublicKey))

		// Derive the public key from the private scalar
		derivedPublicKey := ed25519.NewKeyFromSeed(privateScalar).Public().(ed25519.PublicKey)

		// Encode the derived public key in Base58
		derivedPublicKeyBase58 := base58.Encode(derivedPublicKey)

		// Compare with the actual public key of the account
		fmt.Println("Derived Public Key (Base58:", derivedPublicKeyBase58)
	}
	/*
	Extended Private Key: 62EDyeFBj4ZgaATLPPC3jVikDZaALq5sBs2XKTKwKwAanES8aECVkVKa4xSjRkai9bm1oHvHxJY3T3Vp2NFXJuZA
	Extracted Public Key from Private Key: 8XXKGNRG1qgExJ3irbU8YxYX85xLzvrMBckaYNavoqXv
	Derived Public Key (Base58: 8XXKGNRG1qgExJ3irbU8YxYX85xLzvrMBckaYNavoqXv
	*/

	// from a base58 pirvate key
	{
		account, _ := types.AccountFromBase58("28WJTTqMuurAfz6yqeTrFMXeFd91uzi9i1AW6F5KyHQDS9siXb8TquAuatvLuCEYdggyeiNKLAUr3w7Czmmf2Rav")
		fmt.Println(account.PublicKey.ToBase58())
	}

	// from a private key bytes
	{
		account, _ := types.AccountFromBytes([]byte{
			56, 125, 59, 118, 230, 173, 152, 169, 197, 34,
			168, 187, 217, 160, 119, 204, 124, 69, 52, 136,
			214, 49, 207, 234, 79, 70, 83, 224, 1, 224, 36,
			247, 131, 83, 164, 85, 139, 215, 183, 148, 79,
			198, 74, 93, 156, 157, 208, 99, 221, 127, 51,
			156, 43, 196, 101, 144, 104, 252, 221, 108,
			245, 104, 13, 151,
		})
		fmt.Println(account.PublicKey.ToBase58())
	}

	// from bip 39 (solana cli tool)
	{
		mnemonic := "pill tomorrow foster begin walnut borrow virtual kick shift mutual shoe scatter"
		seed := bip39.NewSeed(mnemonic, "") // (mnemonic, password)
		account, _ := types.AccountFromSeed(seed[:32])
		fmt.Println(account.PublicKey.ToBase58())
	}

	// from bip 44 (phantom)
	{
		mnemonic := "neither lonely flavor argue grass remind eye tag avocado spot unusual intact"
		seed := bip39.NewSeed(mnemonic, "") // (mnemonic, password)
		path := `m/44'/501'/0'/0'`
		derivedKey, _ := hdwallet.Derived(path, seed)
		account, _ := types.AccountFromSeed(derivedKey.PrivateKey)
		fmt.Printf("%v => %v\n", path, account.PublicKey.ToBase58())

		// others
		for i := 1; i < 10; i++ {
			path := fmt.Sprintf(`m/44'/501'/%d'/0'`, i)
			derivedKey, _ := hdwallet.Derived(path, seed)
			account, _ := types.AccountFromSeed(derivedKey.PrivateKey)
			fmt.Printf("%v => %v\n", path, account.PublicKey.ToBase58())
		}
		/*
			m/44'/501'/0'/0' => 5vftMkHL72JaJG6ExQfGAsT2uGVHpRR7oTNUPMs68Y2N
			m/44'/501'/1'/0' => GcXbfQ5yY3uxCyBNDPBbR5FjumHf89E7YHXuULfGDBBv
			m/44'/501'/2'/0' => 7QPgyQwNLqnoSwHEuK8wKy2Y3Ani6EHoZRihTuWkwxbc
			m/44'/501'/3'/0' => 5aE8UprEEWtpVskhxo3f8ETco2kVKiZT9SS3D5Lcg8s2
			m/44'/501'/4'/0' => 5n6afo6LZmzH1J4R38ZCaNSwaztLjd48nWwToLQkCHxp
			m/44'/501'/5'/0' => 2Gr1hWnbaqGXMghicSTHncqV7GVLLddNFJDC7YJoso8M
			m/44'/501'/6'/0' => BNMDY3tCyYbayMzBjZm8RW59unpDWcQRfVmWXCJhLb7D
			m/44'/501'/7'/0' => 9CySTpi4iC85gMW6G4BMoYbNBsdyJrfseHoGmViLha63
			m/44'/501'/8'/0' => ApteF7PmUWS8Lzm6tJPkWgrxSFW5LwYGWCUJ2ByAec91
			m/44'/501'/9'/0' => 6frdqXQAgJMyKwmZxkLYbdGjnYTvUceh6LNhkQt2siQp
		*/
	}
}

Program

The smart contract in Solana we call it program. The program don't store any data. We store data in the account owned by the program. In Solana, every account has a program as an owner. Only the owner can modify the account's data. Takes the token program as an example. You will create an account belongs to the token program and you will transfer token in the token program. The token account will store your balance back to the account it owned.

Instruction

  • Outer instruction
  • Inner instruction
 

Faucet

Request airdrop in Devnet:
package main

import (
	"context"
	"fmt"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/rpc"
	"github.com/blocto/solana-go-sdk/types"
	"github.com/mr-tron/base58"
	"log"
)

func main() {
	c := client.NewClient(rpc.DevnetRPCEndpoint)
	account := types.NewAccount()
	fmt.Printf("created account: %v, private key: %v\n", account.PublicKey.ToBase58(), base58.Encode(account.PrivateKey))
	sig, err := c.RequestAirdrop(context.TODO(), account.PublicKey.ToBase58(), 1e9)
	if err != nil {
		log.Fatalf("failed to request airdrop, err: %v", err)
	}
	fmt.Printf("requested airdrop, signature: %v\n", sig)
	fmt.Printf("check tx at: https://explorer.solana.com/tx/%s?cluster=devnet\n", sig)
}

/* alice
created account: HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg, private key: 5ob5v9uGyJstuENC4pu7ScCdkCPiAZXqFLhu3qUoHB8uePLchCpPmgWyXPZ24NxLBD8dUP6UFNNYXKsFJtFKie74
requested airdrop, signature: 53scqsHpne23owvYdAaZq5DWtzpFtvYW5w64VybrZQ3cuqRojZNzzhQ7EEUL6K26m1FwzNAgLd5yWzP4cPqnuEw2
check tx at: https://explorer.solana.com/tx/53scqsHpne23owvYdAaZq5DWtzpFtvYW5w64VybrZQ3cuqRojZNzzhQ7EEUL6K26m1FwzNAgLd5yWzP4cPqnuEw2?cluster=devnet
*/

/* bob
created account: GLndC8XmRT5o6oBLwn8scDNvFY5MuX78wxJQsW5tXctk, private key: D8i1DFhgxWkBC52kRUtDRkZL5J5bUDJXssJtsehWYT51txphk8ipWe8goFKJt6638vAmEHVxdovsjmfiHPvKPbS
requested airdrop, signature: 2r8VvroDMH9qpGbswZuKt7DaKx12fyzTkc7xJ9Th2KBrT14sYViEd3sHnw67EE6HppXKHZLWKXTsqMRP6kYZ6Lc4
check tx at: https://explorer.solana.com/tx/2r8VvroDMH9qpGbswZuKt7DaKx12fyzTkc7xJ9Th2KBrT14sYViEd3sHnw67EE6HppXKHZLWKXTsqMRP6kYZ6Lc4?cluster=devnet
*/
 
Please note, do not excess the airdrop limit:
{"jsonrpc":"2.0","error":{"code": 429,"message":"You have requested too many airdrops. Wait 24 hours for a refill."}, "id": 1 }
 
Check airdrop in Solana Explorer:
notion image

Check Sol Balance

package main

import (
	"context"
	"fmt"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/rpc"
	"log"
)

func main() {
	c := client.NewClient(rpc.DevnetRPCEndpoint)
	balance, err := c.GetBalance(
		context.TODO(),
		"HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg",
	)
	if err != nil {
		log.Fatalf("get balance, err: %v", err)
	}
	fmt.Println(balance)
}

Transfer

Transfer 0.1 SOL from Alice to Frank, using feePayer to pay for the transaction fee:
package main

import (
	"context"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/program/system"
	"github.com/blocto/solana-go-sdk/rpc"
	"github.com/blocto/solana-go-sdk/types"
	"github.com/mr-tron/base58"
	"log"
)

// HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg
var alice, _ = types.AccountFromBase58("5ob5v9uGyJstuENC4pu7ScCdkCPiAZXqFLhu3qUoHB8uePLchCpPmgWyXPZ24NxLBD8dUP6UFNNYXKsFJtFKie74")

// GLndC8XmRT5o6oBLwn8scDNvFY5MuX78wxJQsW5tXctk
var feePayer, _ = types.AccountFromBase58("D8i1DFhgxWkBC52kRUtDRkZL5J5bUDJXssJtsehWYT51txphk8ipWe8goFKJt6638vAmEHVxdovsjmfiHPvKPbS")

var frank = types.NewAccount()

// Transfer 0.1 SOL from Alice to Frank, using feePayer to pay for the transaction fee
func main() {
	c := client.NewClient(rpc.DevnetRPCEndpoint)

	// log alice account
	log.Printf("frank account: %v, private key: %v\n", frank.PublicKey.ToBase58(), base58.Encode(frank.PrivateKey))

	// to fetch recent blockHash
	recentBlockHashResponse, err := c.GetLatestBlockhash(context.Background())
	if err != nil {
		log.Fatalf("failed to get recent blockhash, err: %v", err)
	}

	// create a transfer tx
	tx, err := types.NewTransaction(types.NewTransactionParam{
		Signers: []types.Account{feePayer, alice},
		Message: types.NewMessage(types.NewMessageParam{
			FeePayer:        feePayer.PublicKey,
			RecentBlockhash: recentBlockHashResponse.Blockhash,
			Instructions: []types.Instruction{
				system.Transfer(system.TransferParam{
					From:   alice.PublicKey,
					To:     frank.PublicKey,
					Amount: 1e8, // 0.1 SOL
				}),
			},
		}),
	})
	if err != nil {
		log.Fatalf("failed to new a transaction, err: %v", err)
	}

	// send tx
	sig, err := c.SendTransaction(context.Background(), tx)
	if err != nil {
		log.Fatalf("failed to send tx, err: %v", err)
	}

	log.Println("signature:", sig)
}

/*
2024/09/04 15:56:38 frank account: 6c7QhVGAvyoGa13gE28jppWGmQ3zEYbvjFxgpVYqE3XL, private key: 4qb6d5onbSDRiR6HzfM4nD8vwuEEG68ZEDQ8626668vUKC33YtYXS9Y9M8K9khDr9ECAH8yrdUsHYYWKY1UUH73t
2024/09/04 15:56:39 signature: 4ZBJsUk3hUKgwE3onS87aJfvNZ38aNEYttzpTbcv6Eew7oUhifdaQG11a7DhBNqkcU1CQgB43pWKXSL5op1LdrGf
*/
 
Both the feePayer and fromAddress need to sign the tx:
notion image
 
A transaction can composed by many instructions so you can do something like A => B, B => C, C => A and D is the fee payer.

Rent

You need to pay a rent if you want to keep your account alive. The number depending on how much size you use. Luckily there is a way to get the rent exemption. You can use RPC getMinimumBalanceForRentExemption to get the number. When your balance in the account bigger than or equals to the number which worth 2 years rent, which is called rent-exemption threshold , if the account becomes rent-exempt, it remains alive indefinitely, as long as no one withdraws from the account and it continues to hold enough balance to cover that threshold.
If an account on Solana runs out of rent (meaning its balance falls below the required threshold and it is not rent-exempt), the account will eventually be deleted when the Solana runtime performs its rent collection. This process is known as account eviction. Once an account on Solana is evicted, you cannot reactivate it. The account is permanently removed, and all data associated with it is lost.

Token

Solana have some pre-defined programs, you can find all the predefined programs as follow:
var (
	SystemProgramID                    = PublicKeyFromString("11111111111111111111111111111111")
	ConfigProgramID                    = PublicKeyFromString("Config1111111111111111111111111111111111111")
	StakeProgramID                     = PublicKeyFromString("Stake11111111111111111111111111111111111111")
	VoteProgramID                      = PublicKeyFromString("Vote111111111111111111111111111111111111111")
	BPFLoaderProgramID                 = PublicKeyFromString("BPFLoader1111111111111111111111111111111111")
	Secp256k1ProgramID                 = PublicKeyFromString("KeccakSecp256k11111111111111111111111111111")
	TokenProgramID                     = PublicKeyFromString("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
	MemoProgramID                      = PublicKeyFromString("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr")
	SPLAssociatedTokenAccountProgramID = PublicKeyFromString("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL")
	SPLNameServiceProgramID            = PublicKeyFromString("namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX")
	MetaplexTokenMetaProgramID         = PublicKeyFromString("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s")
	ComputeBudgetProgramID             = PublicKeyFromString("ComputeBudget111111111111111111111111111111")
	AddressLookupTableProgramID        = PublicKeyFromString("AddressLookupTab1e1111111111111111111111111")
	Token2022ProgramID                 = PublicKeyFromString("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")
	BPFLoaderUpgradeableProgramID      = PublicKeyFromString("BPFLoaderUpgradeab1e11111111111111111111111")
)

SPL token

SPL tokens, or Solana Program Library tokens, are the standard token implementation on the Solana blockchain, the Token Program ID TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA is a crucial identifier in the Solana ecosystem.
What kind of data be stored in token account?
type TokenAccount struct {
	Mint     common.PublicKey
	Owner    common.PublicKey
	Amount   uint64
	Delegate *common.PublicKey
	State    TokenAccountState
	// if is wrapped SOL, IsNative is the rent-exempt value
	IsNative        *uint64
	DelegatedAmount uint64
	CloseAuthority  *common.PublicKey
}
 
  1. Core Instructions:
    1. pub fn initialize_mint(
          mint: &Pubkey,
          mint_authority: &Pubkey,
          freeze_authority: Option<&Pubkey>,
          decimals: u8,
      ) -> Instruction;
      
      pub fn initialize_account(
          account: &Pubkey,
          mint: &Pubkey,
          owner: &Pubkey,
      ) -> Instruction;
      
      pub fn initialize_multisig(
          account: &Pubkey,
          signers: &[&Pubkey],
          m: u8,
      ) -> Instruction;
      
      pub fn transfer(
          source: &Pubkey,
          destination: &Pubkey,
          authority: &Pubkey,
          amount: u64,
      ) -> Instruction;
      
      pub fn approve(
          source: &Pubkey,
          delegate: &Pubkey,
          authority: &Pubkey,
          amount: u64,
      ) -> Instruction;
      
      pub fn revoke(
          source: &Pubkey,
          authority: &Pubkey,
      ) -> Instruction;
      
      pub fn set_authority(
          account_or_mint: &Pubkey,
          current_authority: &Pubkey,
          authority_type: AuthorityType,
          new_authority: Option<&Pubkey>,
      ) -> Instruction;
      
      pub fn mint_to(
          mint: &Pubkey,
          account: &Pubkey,
          authority: &Pubkey,
          amount: u64,
      ) -> Instruction;
      
      pub fn burn(
          account: &Pubkey,
          mint: &Pubkey,
          authority: &Pubkey,
          amount: u64,
      ) -> Instruction;
      
      pub fn close_account(
          account: &Pubkey,
          destination: &Pubkey,
          authority: &Pubkey,
      ) -> Instruction;
      
      pub fn freeze_account(
          account: &Pubkey,
          mint: &Pubkey,
          authority: &Pubkey,
      ) -> Instruction;
      
      pub fn thaw_account(
          account: &Pubkey,
          mint: &Pubkey,
          authority: &Pubkey,
      ) -> Instruction;
      
      pub fn transfer_checked(
          source: &Pubkey,
          mint: &Pubkey,
          destination: &Pubkey,
          authority: &Pubkey,
          amount: u64,
          decimals: u8,
      ) -> Instruction;
      
      pub fn approve_checked(
          source: &Pubkey,
          mint: &Pubkey,
          delegate: &Pubkey,
          authority: &Pubkey,
          amount: u64,
          decimals: u8,
      ) -> Instruction;
      
      pub fn mint_to_checked(
          mint: &Pubkey,
          account: &Pubkey,
          authority: &Pubkey,
          amount: u64,
          decimals: u8,
      ) -> Instruction;
      
      pub fn burn_checked(
          account: &Pubkey,
          mint: &Pubkey,
          authority: &Pubkey,
          amount: u64,
          decimals: u8,
      ) -> Instruction;
      
      pub fn sync_native(account: &Pubkey) -> Instruction;
  1. Account Structures:
      • Mint Account
      • Token Account
  1. Associated Token Accounts (ATAs):
      • Standard way to derive token accounts for users
  1. Metadata (via Metaplex):
      • While not part of the core Token Program, it's a widely adopted standard.
  1. Important Public Keys:
      • Token Program ID: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
      • Associated Token Program ID: ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL

Create SPL token

  1. Create a mint account, the token owner is set to be the Token Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
  1. Initialize mint, set up a new mint account for a specific token, so the mint account stores the following data(which is defined in here.):
    1. pub struct Mint {
          /// Optional authority used to mint new tokens. The mint authority may only
          /// be provided during mint creation. If no mint authority is present
          /// then the mint has a fixed supply and no further tokens may be
          /// minted.
          pub mint_authority: COption<Pubkey>,
          /// Total supply of tokens.
          pub supply: u64,
          /// Number of base 10 digits to the right of the decimal place.
          pub decimals: u8,
          /// Is `true` if this structure has been initialized
          pub is_initialized: bool,
          /// Optional authority to freeze token accounts.
          pub freeze_authority: COption<Pubkey>,
      }
 
Full Code:
package main

import (
	"context"
	"fmt"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/system"
	"github.com/blocto/solana-go-sdk/program/token"
	"github.com/blocto/solana-go-sdk/rpc"
	"github.com/blocto/solana-go-sdk/types"
	"log"
)

// GLndC8XmRT5o6oBLwn8scDNvFY5MuX78wxJQsW5tXctk
var feePayer, _ = types.AccountFromBase58("D8i1DFhgxWkBC52kRUtDRkZL5J5bUDJXssJtsehWYT51txphk8ipWe8goFKJt6638vAmEHVxdovsjmfiHPvKPbS")

// HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg
var alice, _ = types.AccountFromBase58("5ob5v9uGyJstuENC4pu7ScCdkCPiAZXqFLhu3qUoHB8uePLchCpPmgWyXPZ24NxLBD8dUP6UFNNYXKsFJtFKie74")

func main() {
	c := client.NewClient(rpc.DevnetRPCEndpoint)

	// create a mint account
	mint := types.NewAccount()
	fmt.Println("mint:", mint.PublicKey.ToBase58())

	// get rent
	rentExemptionBalance, err := c.GetMinimumBalanceForRentExemption(
		context.Background(),
		token.MintAccountSize,
	)
	if err != nil {
		log.Fatalf("get min balacne for rent exemption, err: %v", err)
	}

	res, err := c.GetLatestBlockhash(context.Background())
	if err != nil {
		log.Fatalf("get recent block hash error, err: %v\n", err)
	}

	tx, err := types.NewTransaction(types.NewTransactionParam{
		Message: types.NewMessage(types.NewMessageParam{
			FeePayer:        feePayer.PublicKey,
			RecentBlockhash: res.Blockhash,
			Instructions: []types.Instruction{
				system.CreateAccount(system.CreateAccountParam{
					From:     feePayer.PublicKey,
					New:      mint.PublicKey,
					Owner:    common.TokenProgramID,
					Lamports: rentExemptionBalance,
					Space:    token.MintAccountSize,
				}),
				token.InitializeMint(token.InitializeMintParam{
					Decimals:   8,
					Mint:       mint.PublicKey,
					MintAuth:   alice.PublicKey,
					FreezeAuth: nil,
				}),
			},
		}),
		Signers: []types.Account{feePayer, mint},
	})
	if err != nil {
		log.Fatalf("generate tx error, err: %v\n", err)
	}

	sig, err := c.SendTransaction(context.Background(), tx)
	if err != nil {
		log.Fatalf("send tx error, err: %v\n", err)
	}

	fmt.Printf("check tx at: https://explorer.solana.com/tx/%s?cluster=devnet\n", sig)
}

/*
mint: gYqzga5v1RoVWxtfXizHuoyxUpTnzf9WyrXftTkDfpT
check tx at: https://explorer.solana.com/tx/2zfGVnNkfEWL91emjAfSb3AfFh7XJK4FvnYAiMgibv4iEddjMLsdQGSvjjiGziXVTKBGVWKAMjnKfzXKYx6ZdXHx?cluster=devnet
*/
 
View the two instructions in Explorer:
notion image
 
View the token page:
notion image

Get mint

Get detail from a exist mint:
package main

import (
	"context"
	"fmt"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/token"
	"github.com/blocto/solana-go-sdk/rpc"
	"log"
)

var mintPubkey = common.PublicKeyFromString("gYqzga5v1RoVWxtfXizHuoyxUpTnzf9WyrXftTkDfpT")

func main() {
	c := client.NewClient(rpc.DevnetRPCEndpoint)

	getAccountInfoResponse, err := c.GetAccountInfo(context.TODO(), mintPubkey.ToBase58())
	if err != nil {
		log.Fatalf("failed to get account info, err: %v", err)
	}

	mintAccount, err := token.MintAccountFromData(getAccountInfoResponse.Data)
	if err != nil {
		log.Fatalf("failed to parse data to a mint account, err: %v", err)
	}

	fmt.Printf("%+v\n", mintAccount)
	// {MintAuthority:HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg Supply:0 Decimals:8 IsInitialized:true FreezeAuthority:<nil>}
}

Create token account

Create Associated Token Account(Recommended):
package main

import (
	"context"
	"fmt"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/associated_token_account"
	"github.com/blocto/solana-go-sdk/rpc"
	"github.com/blocto/solana-go-sdk/types"
	"log"
)

// GLndC8XmRT5o6oBLwn8scDNvFY5MuX78wxJQsW5tXctk
var feePayer, _ = types.AccountFromBase58("D8i1DFhgxWkBC52kRUtDRkZL5J5bUDJXssJtsehWYT51txphk8ipWe8goFKJt6638vAmEHVxdovsjmfiHPvKPbS")

// HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg
var alice, _ = types.AccountFromBase58("5ob5v9uGyJstuENC4pu7ScCdkCPiAZXqFLhu3qUoHB8uePLchCpPmgWyXPZ24NxLBD8dUP6UFNNYXKsFJtFKie74")

var mintPubkey = common.PublicKeyFromString("gYqzga5v1RoVWxtfXizHuoyxUpTnzf9WyrXftTkDfpT")

func main() {
	c := client.NewClient(rpc.DevnetRPCEndpoint)

	ata, _, err := common.FindAssociatedTokenAddress(alice.PublicKey, mintPubkey)
	if err != nil {
		log.Fatalf("find ata error, err: %v", err)
	}
	fmt.Println("ata:", ata.ToBase58())

	res, err := c.GetLatestBlockhash(context.Background())
	if err != nil {
		log.Fatalf("get recent block hash error, err: %v\n", err)
	}

	tx, err := types.NewTransaction(types.NewTransactionParam{
		Message: types.NewMessage(types.NewMessageParam{
			FeePayer:        feePayer.PublicKey,
			RecentBlockhash: res.Blockhash,
			Instructions: []types.Instruction{
				associated_token_account.Create(associated_token_account.CreateParam{
					Funder:                 feePayer.PublicKey,
					Owner:                  alice.PublicKey,
					Mint:                   mintPubkey,
					AssociatedTokenAccount: ata,
				}),
			},
		}),
		Signers: []types.Account{feePayer},
	})
	if err != nil {
		log.Fatalf("generate tx error, err: %v\n", err)
	}

	txhash, err := c.SendTransaction(context.Background(), tx)
	if err != nil {
		log.Fatalf("send raw tx error, err: %v\n", err)
	}

	log.Println("txhash:", txhash)
}

/*
ata: BdEcBm46DWCEBFXVHwXhW76RLqzyCpaiJMxgveL8dLEm
2024/09/05 17:35:43 txhash: 4o9WR46XCB5ovESWzcTzc66DLvk5nmTWfi26iDvLWycgDD6FjqFLLBrdyDoADhuq6JMxCqUmXHGBtQB8kBxUGg62
*/

Mint to

package main

import (
	"context"
	"fmt"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/token"
	"github.com/blocto/solana-go-sdk/rpc"
	"github.com/blocto/solana-go-sdk/types"
	"log"
)

// GLndC8XmRT5o6oBLwn8scDNvFY5MuX78wxJQsW5tXctk
var feePayer, _ = types.AccountFromBase58("D8i1DFhgxWkBC52kRUtDRkZL5J5bUDJXssJtsehWYT51txphk8ipWe8goFKJt6638vAmEHVxdovsjmfiHPvKPbS")

// HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg
var alice, _ = types.AccountFromBase58("5ob5v9uGyJstuENC4pu7ScCdkCPiAZXqFLhu3qUoHB8uePLchCpPmgWyXPZ24NxLBD8dUP6UFNNYXKsFJtFKie74")

var mintPubkey = common.PublicKeyFromString("gYqzga5v1RoVWxtfXizHuoyxUpTnzf9WyrXftTkDfpT")

var aliceTokenATAPubkey = common.PublicKeyFromString("BdEcBm46DWCEBFXVHwXhW76RLqzyCpaiJMxgveL8dLEm")

func main() {
	c := client.NewClient(rpc.DevnetRPCEndpoint)

	res, err := c.GetLatestBlockhash(context.Background())
	if err != nil {
		log.Fatalf("get recent block hash error, err: %v\n", err)
	}

	tx, err := types.NewTransaction(types.NewTransactionParam{
		Message: types.NewMessage(types.NewMessageParam{
			FeePayer:        feePayer.PublicKey,
			RecentBlockhash: res.Blockhash,
			Instructions: []types.Instruction{
				token.MintToChecked(token.MintToCheckedParam{
					Mint:     mintPubkey,
					Auth:     alice.PublicKey,
					Signers:  []common.PublicKey{},
					To:       aliceTokenATAPubkey,
					Amount:   1e8,
					Decimals: 8,
				}),
			},
		}),
		Signers: []types.Account{feePayer, alice},
	})
	if err != nil {
		log.Fatalf("generate tx error, err: %v\n", err)
	}

	txhash, err := c.SendTransaction(context.Background(), tx)
	if err != nil {
		log.Fatalf("send raw tx error, err: %v\n", err)
	}

	fmt.Printf("check tx at: https://explorer.solana.com/tx/%s?cluster=devnet\n", txhash)
}

/*
check tx at: https://explorer.solana.com/tx/3TP1UDkbWWKurLmBSCyP6nii6J2vghxsWRtSmANMjPN1NsdNzSoZQhyPjdxQd4TeAkeGyFX1qMu8wVKRchA2QQP5?cluster=devnet
*/

Get token account

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/program/token"
	"github.com/blocto/solana-go-sdk/rpc"
)

func main() {
	c := client.NewClient(rpc.DevnetRPCEndpoint)

	// token account address
	getAccountInfoResponse, err := c.GetAccountInfo(context.TODO(), "BdEcBm46DWCEBFXVHwXhW76RLqzyCpaiJMxgveL8dLEm")
	if err != nil {
		log.Fatalf("failed to get account info, err: %v", err)
	}

	tokenAccount, err := token.TokenAccountFromData(getAccountInfoResponse.Data)
	if err != nil {
		log.Fatalf("failed to parse data to a token account, err: %v", err)
	}

	fmt.Printf("%+v\n", tokenAccount)
	// before mint to the account {Mint:gYqzga5v1RoVWxtfXizHuoyxUpTnzf9WyrXftTkDfpT Owner:HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg Amount:0 Delegate:<nil> State:1 IsNative:<nil> DelegatedAmount:0 CloseAuthority:<nil>}
	// after mint to the account {Mint:gYqzga5v1RoVWxtfXizHuoyxUpTnzf9WyrXftTkDfpT Owner:HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg Amount:100000000 Delegate:<nil> State:1 IsNative:<nil> DelegatedAmount:0 CloseAuthority:<nil>}
}
 

Get token mint from ATA

 
package main

import (
	"context"
	"fmt"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/token"
	"github.com/blocto/solana-go-sdk/rpc"
)

func main() {
	c := client.NewClient(rpc.MainnetRPCEndpoint)
	mint, err := getTokenMintFromATA(c, common.PublicKeyFromString("3mHBG2nm6Y9inWayRE7qgfeYMocaoZScfAxizWf19zrS"))
	if err != nil {
		return
	}
	fmt.Println(mint)
}

// output: gXduukdwXJbVw1AjpPcnzmiPFxFHTPSE8yL74LUDfgC

func getTokenMintFromATA(c *client.Client, ataAddress common.PublicKey) (common.PublicKey, error) {
	accountInfo, err := c.GetAccountInfo(context.TODO(), ataAddress.ToBase58())
	if err != nil {
		return common.PublicKey{}, err
	}

	ataInfo, err := token.TokenAccountFromData(accountInfo.Data)
	if err != nil {
		return common.PublicKey{}, fmt.Errorf("failed to parse token account data: %w", err)
	}

	return ataInfo.Mint, nil
}

Transfer token

The WRONG way to transfer token to a newly created account:
package main

import (
	"context"
	"fmt"
	"github.com/mr-tron/base58"
	"log"

	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/token"
	"github.com/blocto/solana-go-sdk/rpc"
	"github.com/blocto/solana-go-sdk/types"
)

// GLndC8XmRT5o6oBLwn8scDNvFY5MuX78wxJQsW5tXctk
var feePayer, _ = types.AccountFromBase58("D8i1DFhgxWkBC52kRUtDRkZL5J5bUDJXssJtsehWYT51txphk8ipWe8goFKJt6638vAmEHVxdovsjmfiHPvKPbS")

// HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg
var alice, _ = types.AccountFromBase58("5ob5v9uGyJstuENC4pu7ScCdkCPiAZXqFLhu3qUoHB8uePLchCpPmgWyXPZ24NxLBD8dUP6UFNNYXKsFJtFKie74")

var mintPubkey = common.PublicKeyFromString("gYqzga5v1RoVWxtfXizHuoyxUpTnzf9WyrXftTkDfpT")

var aliceTokenATAPubkey = common.PublicKeyFromString("BdEcBm46DWCEBFXVHwXhW76RLqzyCpaiJMxgveL8dLEm")

func main() {
	c := client.NewClient(rpc.DevnetRPCEndpoint)

	res, err := c.GetLatestBlockhash(context.Background())
	if err != nil {
		log.Fatalf("get recent block hash error, err: %v\n", err)
	}

	newAccount := types.NewAccount()
	log.Println("new account:", newAccount.PublicKey.ToBase58(), base58.Encode(newAccount.PrivateKey))

	tx, err := types.NewTransaction(types.NewTransactionParam{
		Message: types.NewMessage(types.NewMessageParam{
			FeePayer:        feePayer.PublicKey,
			RecentBlockhash: res.Blockhash,
			Instructions: []types.Instruction{
				token.TransferChecked(token.TransferCheckedParam{
					From:     aliceTokenATAPubkey,
					To:       newAccount.PublicKey,
					Mint:     mintPubkey,
					Auth:     alice.PublicKey,
					Signers:  []common.PublicKey{},
					Amount:   1e7,
					Decimals: 8,
				}),
			},
		}),
		Signers: []types.Account{feePayer, alice},
	})
	if err != nil {
		log.Fatalf("failed to new tx, err: %v", err)
	}

	txhash, err := c.SendTransaction(context.Background(), tx)
	if err != nil {
		log.Fatalf("send raw tx error, err: %v\n", err)
	}

	fmt.Printf("check tx at: https://explorer.solana.com/tx/%s?cluster=devnet\n", txhash)
}

/*
2024/09/06 10:55:37 new account: 49mFqeNosQqDk3aj332ayCtmBdVU85utcWAjggZaW8Lw 35Y1JSqrMvcN2kAczXGWxJiwJ2V2MyYVBDsfaVV2BbFhLCmc1SWToaGmwRtbsGxDpf9bK2Uf1fj8HLFFYLzBYDi7
2024/09/06 10:55:37 send raw tx error, err: {"code":-32002,"message":"Transaction simulation failed: Error processing Instruction 0: invalid account data for instruction","data":{"accounts":null,"err":{"InstructionError":[0,"InvalidAccountData"]},"innerInstructions":null,"logs":["Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [1]","Program log: Instruction: TransferChecked","Program log: Error: InvalidAccountData","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 2985 of 200000 compute units","Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA failed: invalid account data for instruction"],"replacementBlockhash":null,"returnData":null,"unitsConsumed":2985}}
*/
 
The right way to transfer token to a new account, is to transfer account’s Associated Token Account. But we need to initialize it before transfer:
package main

import (
	"context"
	"fmt"
	"github.com/blocto/solana-go-sdk/program/associated_token_account"
	"github.com/mr-tron/base58"
	"log"

	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/token"
	"github.com/blocto/solana-go-sdk/rpc"
	"github.com/blocto/solana-go-sdk/types"
)

// GLndC8XmRT5o6oBLwn8scDNvFY5MuX78wxJQsW5tXctk
var feePayer, _ = types.AccountFromBase58("D8i1DFhgxWkBC52kRUtDRkZL5J5bUDJXssJtsehWYT51txphk8ipWe8goFKJt6638vAmEHVxdovsjmfiHPvKPbS")

// HcNCxoni2Ln5si48s1w8r5TRVH296RQ1MzKeM9FctdPg
var alice, _ = types.AccountFromBase58("5ob5v9uGyJstuENC4pu7ScCdkCPiAZXqFLhu3qUoHB8uePLchCpPmgWyXPZ24NxLBD8dUP6UFNNYXKsFJtFKie74")

var mintPubkey = common.PublicKeyFromString("gYqzga5v1RoVWxtfXizHuoyxUpTnzf9WyrXftTkDfpT")

var aliceTokenATAPubkey = common.PublicKeyFromString("BdEcBm46DWCEBFXVHwXhW76RLqzyCpaiJMxgveL8dLEm")

func main() {
	c := client.NewClient(rpc.DevnetRPCEndpoint)

	res, err := c.GetLatestBlockhash(context.Background())
	if err != nil {
		log.Fatalf("get recent block hash error, err: %v\n", err)
	}

	newAccount := types.NewAccount()
	log.Println("new account:", newAccount.PublicKey.ToBase58(), base58.Encode(newAccount.PrivateKey))

	ata, _, err := common.FindAssociatedTokenAddress(newAccount.PublicKey, mintPubkey)
	if err != nil {
		log.Fatalf("find ata error, err: %v", err)
	}

	tx, err := types.NewTransaction(types.NewTransactionParam{
		Message: types.NewMessage(types.NewMessageParam{
			FeePayer:        feePayer.PublicKey,
			RecentBlockhash: res.Blockhash,
			Instructions: []types.Instruction{
				associated_token_account.Create(associated_token_account.CreateParam{
					Funder:                 feePayer.PublicKey,
					Owner:                  newAccount.PublicKey,
					Mint:                   mintPubkey,
					AssociatedTokenAccount: ata,
				}),
				token.TransferChecked(token.TransferCheckedParam{
					From:     aliceTokenATAPubkey,
					To:       ata,
					Mint:     mintPubkey,
					Auth:     alice.PublicKey,
					Signers:  []common.PublicKey{},
					Amount:   1e7,
					Decimals: 8,
				}),
			},
		}),
		Signers: []types.Account{feePayer, alice},
	})
	if err != nil {
		log.Fatalf("failed to new tx, err: %v", err)
	}

	txhash, err := c.SendTransaction(context.Background(), tx)
	if err != nil {
		log.Fatalf("send raw tx error, err: %v\n", err)
	}

	fmt.Printf("check tx at: https://explorer.solana.com/tx/%s?cluster=devnet\n", txhash)
}

/*
2024/09/06 11:37:28 new account: DfHYap9MpUyNjEkVKUC8jwwy7TrsPWrWnaAMzdZUroAg 59UvmLfepKgEoUdGWjjachF3neFuqRZkrmo6BgBn1EMWg7squTRZxuCB3L9zRT8bBzdU9QaLHhR5byEroVFx4fC4
check tx at: https://explorer.solana.com/tx/4Wo87ndvevGXEprhu4UwBHC3GxpFQiW925uomUFt2yVHk1J61rzZXcSmjTPn73XyhA3TncoXUNZAJxpxkguD1jtF?cluster=devnet
*/
 
notion image
 

Set token metadata

 
package main

import (
	"context"
	"fmt"
	"log"

	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/metaplex/token_metadata"
	"github.com/blocto/solana-go-sdk/rpc"
	"github.com/blocto/solana-go-sdk/types"
)

// Fee payer account (replace with your own)
var feePayer, _ = types.AccountFromBase58("D8i1DFhgxWkBC52kRUtDRkZL5J5bUDJXssJtsehWYT51txphk8ipWe8goFKJt6638vAmEHVxdovsjmfiHPvKPbS")

// Alice account (replace with your own)
var alice, _ = types.AccountFromBase58("5ob5v9uGyJstuENC4pu7ScCdkCPiAZXqFLhu3qUoHB8uePLchCpPmgWyXPZ24NxLBD8dUP6UFNNYXKsFJtFKie74")

// Token Mint Pubkey (replace with your own token mint public key)
var mintPubkey = common.PublicKeyFromString("gYqzga5v1RoVWxtfXizHuoyxUpTnzf9WyrXftTkDfpT")

// Function to set token metadata
func setTokenMetadata(c *client.Client, mintPubkey common.PublicKey, data token_metadata.DataV2) error {
	// Get the metadata account for the mint
	metadataAccount, err := token_metadata.GetTokenMetaPubkey(mintPubkey)
	if err != nil {
		return fmt.Errorf("failed to get metadata account: %v", err)
	}

	// Fetch the latest blockhash for the transaction
	res, err := c.GetLatestBlockhash(context.Background())
	if err != nil {
		return fmt.Errorf("failed to get recent blockhash: %v", err)
	}

	// Create Metadata Account using CreateMetadataAccountV2
	createMetadataAccount := token_metadata.CreateMetadataAccountV3(token_metadata.CreateMetadataAccountV3Param{
		Metadata:      metadataAccount,
		Mint:          mintPubkey,
		MintAuthority: alice.PublicKey,
		Payer:         feePayer.PublicKey,
		Data:          data,
		IsMutable:     true,
	})

	// Create a transaction with the given instructions
	tx, err := types.NewTransaction(types.NewTransactionParam{
		Message: types.NewMessage(types.NewMessageParam{
			FeePayer:        feePayer.PublicKey,
			RecentBlockhash: res.Blockhash,
			Instructions:    []types.Instruction{createMetadataAccount},
		}),
		Signers: []types.Account{feePayer, alice}, // Both feePayer and mintAuthority (alice) need to sign
	})
	if err != nil {
		return fmt.Errorf("failed to create transaction: %v", err)
	}

	// Send the transaction
	sig, err := c.SendTransaction(context.Background(), tx)
	if err != nil {
		return fmt.Errorf("failed to send transaction: %v", err)
	}

	// Output transaction explorer link
	fmt.Printf("Check the transaction at: https://explorer.solana.com/tx/%s?cluster=devnet\n", sig)

	return nil
}

func main() {
	// Create a new Solana client pointing to the Devnet cluster
	c := client.NewClient(rpc.DevnetRPCEndpoint)

	// Token metadata to be set
	metadataData := token_metadata.DataV2{
		Name:                 "Cool token",            // Token name
		Symbol:               "COOL",                  // Token symbol
		Uri:                  "https://cooltoken.com", // Token URI
		SellerFeeBasisPoints: 0,
		Creators:             nil,
	}

	// Attempt to set the token metadata
	err := setTokenMetadata(c, mintPubkey, metadataData)
	if err != nil {
		log.Fatalf("set token metadata error: %v", err)
	}
}

/*
Check the transaction at: https://explorer.solana.com/tx/En2UPcgjBkn1eP3mV4hGBcVT6GjnMfYeHrEPZcmhwFAkBovBXBPFktTu6mRpAt7uUrCHftSEiqr65hgjdnUJMju?cluster=devnet
*/
 
notion image

Get token metadata

notion image
SPL token’s metadata(including token Symbol and Name) is not stored on mint address, usually token metadata is stored on token metadata account, and token metadata account is derived from the address of Mint Account via Metaplex Token Meta Program.
 
package main

import (
	"context"
	"fmt"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/metaplex/token_metadata"
	"github.com/blocto/solana-go-sdk/rpc"
	"log"
)

const (
	USDCMintAddress = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
)

func GetTokenMetadata(c *client.Client, mintAddress string) (*token_metadata.Metadata, error) {
	mintPubKey := common.PublicKeyFromString(mintAddress)
	metadataAddress, err := token_metadata.GetTokenMetaPubkey(mintPubKey)
	if err != nil {
		return nil, fmt.Errorf("failed to find metadata PDA: %v", err)
	}

	accountInfo, err := c.GetAccountInfo(context.Background(), metadataAddress.ToBase58())
	if err != nil {
		return nil, fmt.Errorf("failed to get account info: %v", err)
	}

	metadata, err := token_metadata.MetadataDeserialize(accountInfo.Data)
	if err != nil {
		log.Fatalf("failed to parse metaAccount, err: %v", err)
	}
	//spew.Dump(metadata)

	return &metadata, nil
}

func main() {
	c := client.NewClient(rpc.MainnetRPCEndpoint)

	tokenMetadata, err := GetTokenMetadata(c, USDCMintAddress)
	if err != nil {
		log.Fatalf("failed to retrieve token metadata: %v", err)
	}
	fmt.Printf("Token Symbol: %s, Token Name: %s\n", tokenMetadata.Data.Symbol, tokenMetadata.Data.Name)
}

/*
Token Symbol: USDC, Token Name: USD Coin
*/
 
notion image
 

Get full token info

In solana, token decimals is defined in token mint address, but a token is not necessary to have a symbol or name.
SPL token’s metadata(including token symbol and name) is not stored on mint address, usually token metadata is stored on token metadata account, and token metadata account is derived from the address of Mint Account via Metaplex Token Meta Program.
In order to get the full token info, we must query mint address as well as the Derived Token Meta Program:
package main

import (
	"context"
	"fmt"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/metaplex/token_metadata"
	"github.com/blocto/solana-go-sdk/rpc"
)

type Token struct {
	Address  string // mint address
	Decimals uint8
	Symbol   string
	Name     string
}

func main() {
	c := client.NewClient(rpc.MainnetRPCEndpoint)
	token, err := newToken(c, "So11111111111111111111111111111111111111112")
	fmt.Println(token, err)
}

/*
output: &{So11111111111111111111111111111111111111112 9 SOL Wrapped SOL} <nil>
*/

func newToken(c *client.Client, mintAddress string) (*Token, error) {
	account, err := c.GetAccountInfo(context.Background(), mintAddress)
	if err != nil {
		return nil, err
	}

	// The decimals are stored at byte offset 44 in the mint account data
	if len(account.Data) < 45 {
		return nil, fmt.Errorf("invalid mint account data")
	}

	decimals := account.Data[44]

	// Get metadata
	mintPubKey := common.PublicKeyFromString(mintAddress)
	metadataAddress, err := token_metadata.GetTokenMetaPubkey(mintPubKey)
	if err != nil {
		return nil, fmt.Errorf("failed to find metadata PDA: %v", err)
	}

	metadataAccountInfo, err := c.GetAccountInfo(context.Background(), metadataAddress.ToBase58())
	if err != nil {
		return nil, fmt.Errorf("failed to get metadata account info: %v", err)
	}

	metadata, err := token_metadata.MetadataDeserialize(metadataAccountInfo.Data)
	if err != nil {
		return nil, fmt.Errorf("failed to deserialize metadata: %v", err)
	}

	return &Token{
		Address:  mintAddress,
		Decimals: decimals,
		Symbol:   metadata.Data.Symbol,
		Name:     metadata.Data.Name,
	}, nil
}

Extract token transfer details

It’s a little bit complex to decode Solana token transfer data, there are several aspects to take into consideration:
  • There are transfer and transferChecked instructions, both can execute a transfer.
  • But for transfer instruction, there’s no token mint address in the context, you should derive the mint address, refers to tryDeriveMintAddress function in the following code. It’s a little bit tricky.
  • Transfer can be execute in outer instructions as well as inner instructions, you should check both.
  • You also need to get the detail token info(decimals, symbol, and name ).
 
package main

import (
	"context"
	"encoding/binary"
	"fmt"
	"github.com/blocto/solana-go-sdk/client"
	"github.com/blocto/solana-go-sdk/common"
	"github.com/blocto/solana-go-sdk/program/metaplex/token_metadata"
	"github.com/blocto/solana-go-sdk/rpc"
	"github.com/blocto/solana-go-sdk/types"
	"github.com/shopspring/decimal"
)

type Transfer struct {
	Type                      string `json:"type"`
	TokenAddress              string `json:"tokenAddress"`
	Decimals                  uint8  `json:"decimals"`
	Symbol                    string `json:"symbol"`
	Name                      string `json:"name"`
	Authority                 string `json:"authority"`
	Source                    string `json:"source"`
	Destination               string `json:"destination"`
	Amount                    string `json:"amount"`
	UiAmount                  string `json:"uiAmount"`
	IsInnerInstruction        bool   `json:"isInnerInstruction"`
	OuterInstructionIndex     int    `json:"outerInstructionIndex"`
	OuterInstructionProgramID string `json:"outerInstructionProgramID"`
}

type Token struct {
	Address  string // mint address
	Decimals uint8
	Symbol   string
	Name     string
}

func main() {
	c := client.NewClient(rpc.MainnetRPCEndpoint)
	txHash := "4yoaptWrZcNuyPujYTCT3xtydveKa6MLxJr9v4Ypmr9uMpLRUubj2xupL3F8KRQwKVi2YLvetS34sQWYw9R4YupF"
	transfers, err := decodeTokenTransferInstruction(c, txHash)
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, transfer := range transfers {
		fmt.Printf("Transfer: %+v\n", *transfer)
	}
}

/* output:
Transfer: {Type:transfer TokenAddress:So11111111111111111111111111111111111111112 Decimals:9 Symbol:SOL Name:Wrapped SOL Authority:DVnVg4p4uzoQfH48iUfx8EGYE2q34xfDzGwwACYDD9G6 Source:6tFPTzVd4Lg3NVgWgwDb7bVfiUcigLXHoBE3Fernjfqw Destination:DzaqzbktzU4PgpXkxpXLWvGH8BAM6P1Q3JjdjEibsHcB Amount:10000 UiAmount:0.00001 IsInnerInstruction:false OuterInstructionIndex:3 OuterInstructionProgramID:TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA}
Transfer: {Type:transfer TokenAddress:So11111111111111111111111111111111111111112 Decimals:9 Symbol:SOL Name:Wrapped SOL Authority:DVnVg4p4uzoQfH48iUfx8EGYE2q34xfDzGwwACYDD9G6 Source:DzaqzbktzU4PgpXkxpXLWvGH8BAM6P1Q3JjdjEibsHcB Destination:BH99eJBXodXtJRCbE4Z2vpashf19pLW9vGf37PTPWH9D Amount:10000 UiAmount:0.00001 IsInnerInstruction:true OuterInstructionIndex:4 OuterInstructionProgramID:675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8}
Transfer: {Type:transfer TokenAddress:1DZ2M31avcvyXMihcX5Pjtcz4qZeGFuQ2gGSjSwoRms Decimals:6 Symbol:WORMS Name:Worms by Matt Furie Authority:5Q544fKrFoe6tsEbD7S8EmxGTJYAKtTVhAW5Q5pge4j1 Source:96RaEiBVEZgpWDCKBhmNMu4E3WiAU1thBGk6NYNqH9eK Destination:FZkQSdvQqWbNh1ASdo9MYUxeUNkoNWdC1Wkv33jGBWLc Amount:10281 UiAmount:0.010281 IsInnerInstruction:true OuterInstructionIndex:4 OuterInstructionProgramID:675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8}
*/

func decodeTokenTransferInstruction(c *client.Client, txHash string) ([]*Transfer, error) {
	// Query transaction details
	tx, err := c.GetTransaction(context.Background(), txHash)
	if err != nil {
		return nil, fmt.Errorf("error fetching transaction details: %w", err)
	}

	// Cache the account index mapping
	var indexAccountMap = make(map[int]string)
	for i, account := range tx.AccountKeys {
		indexAccountMap[i] = account.ToBase58()
	}

	// Cache the instruction programIDIndex mapping
	var instructionIndexProgramIDMap = make(map[int]int)
	for i, instruction := range tx.Transaction.Message.Instructions {
		instructionIndexProgramIDMap[i] = instruction.ProgramIDIndex
	}

	// Cache token mint addresses
	var mintAddresses = make(map[string]struct{})
	for _, tokenBalance := range tx.Meta.PreTokenBalances {
		mintAddresses[tokenBalance.Mint] = struct{}{}
	}

	var allTransfers []*Transfer

	// Process outer instructions
	for i, instruction := range tx.Transaction.Message.Instructions {
		programID := indexAccountMap[instruction.ProgramIDIndex]
		if programID == common.TokenProgramID.String() {
			transfer, _ := tryDecodeTransfer(instruction, indexAccountMap)
			if transfer != nil {
				transfer.IsInnerInstruction = false
				transfer.OuterInstructionIndex = i
				transfer.OuterInstructionProgramID = programID
				allTransfers = append(allTransfers, transfer)
			}
		}
	}

	// Process inner instructions
	for _, innerInstructions := range tx.Meta.InnerInstructions {
		outerProgramIDIndex := instructionIndexProgramIDMap[int(innerInstructions.Index)]
		outerProgramID := indexAccountMap[outerProgramIDIndex]
		for _, instruction := range innerInstructions.Instructions {
			programID := indexAccountMap[instruction.ProgramIDIndex]
			if programID == common.TokenProgramID.String() {
				transfer, _ := tryDecodeTransfer(instruction, indexAccountMap)
				if transfer != nil {
					transfer.IsInnerInstruction = true
					transfer.OuterInstructionIndex = int(innerInstructions.Index)
					transfer.OuterInstructionProgramID = outerProgramID
					allTransfers = append(allTransfers, transfer)
				}
			}
		}
	}

	// Get all the authorities from allTransfers
	var authorities = make(map[string]struct{})
	for _, transfer := range allTransfers {
		authorities[transfer.Authority] = struct{}{}
	}

	// Try to derive mint address for transfers without mint info
	for _, transfer := range allTransfers {
		if transfer.TokenAddress == "" {
			derivedMint := tryDeriveMintAddress(transfer.Source, transfer.Destination, authorities, mintAddresses)
			if derivedMint != "" {
				transfer.TokenAddress = derivedMint
			}
		}
	}

	// Get and populate token info for all transfers(which mint is not empty)
	for _, transfer := range allTransfers {
		if transfer.TokenAddress != "" {
			token, err := newToken(c, transfer.TokenAddress)
			if err == nil && token != nil {
				transfer.Symbol = token.Symbol
				transfer.Name = token.Name
				transfer.Decimals = token.Decimals
				if token.Decimals <= 0 {
					transfer.UiAmount = transfer.Amount
				} else {
					amount, _ := decimal.NewFromString(transfer.Amount)
					divisor := decimal.New(1, int32(token.Decimals))
					uiAmount := amount.Div(divisor)
					transfer.UiAmount = uiAmount.String()
				}
			}
		}

	}

	return allTransfers, nil
}

func newToken(c *client.Client, mintAddress string) (*Token, error) {
	account, err := c.GetAccountInfo(context.Background(), mintAddress)
	if err != nil {
		return nil, err
	}

	// The decimals are stored at byte offset 44 in the mint account data
	if len(account.Data) < 45 {
		return nil, fmt.Errorf("invalid mint account data")
	}

	decimals := account.Data[44]

	// Get metadata
	mintPubKey := common.PublicKeyFromString(mintAddress)
	metadataAddress, err := token_metadata.GetTokenMetaPubkey(mintPubKey)
	if err != nil {
		return nil, fmt.Errorf("failed to find metadata PDA: %v", err)
	}

	metadataAccountInfo, err := c.GetAccountInfo(context.Background(), metadataAddress.ToBase58())
	if err != nil {
		return nil, fmt.Errorf("failed to get metadata account info: %v", err)
	}

	metadata, err := token_metadata.MetadataDeserialize(metadataAccountInfo.Data)
	if err != nil {
		return nil, fmt.Errorf("failed to deserialize metadata: %v", err)
	}

	return &Token{
		Address:  mintAddress,
		Decimals: decimals,
		Symbol:   metadata.Data.Symbol,
		Name:     metadata.Data.Name,
	}, nil
}

func tryDecodeTransfer(instruction types.CompiledInstruction, indexAccountMap map[int]string) (*Transfer, error) {
	if len(instruction.Data) == 0 {
		return nil, nil
	}

	instructionType := instruction.Data[0]

	var amount uint64
	var sourceAccount, destinationAccount, authorityAccount, mintAddress string

	switch instructionType {
	case 3: // Transfer
		if len(instruction.Data) < 9 || len(instruction.Accounts) < 3 {
			return nil, fmt.Errorf("invalid transfer instruction data: data length %d, accounts length %d", len(instruction.Data), len(instruction.Accounts))
		}
		amount = binary.LittleEndian.Uint64(instruction.Data[1:9])
		sourceAccount = indexAccountMap[instruction.Accounts[0]]
		destinationAccount = indexAccountMap[instruction.Accounts[1]]
		authorityAccount = indexAccountMap[instruction.Accounts[2]]

		// We don't have the mint address for regular transfers, so we'll leave it empty
		mintAddress = ""

	case 12: // TransferChecked
		if len(instruction.Data) < 10 || len(instruction.Accounts) < 4 {
			return nil, fmt.Errorf("invalid transfer checked instruction data: data length %d, accounts length %d", len(instruction.Data), len(instruction.Accounts))
		}
		amount = binary.LittleEndian.Uint64(instruction.Data[1:9])
		sourceAccount = indexAccountMap[instruction.Accounts[0]]
		mintAddress = indexAccountMap[instruction.Accounts[1]]
		destinationAccount = indexAccountMap[instruction.Accounts[2]]
		authorityAccount = indexAccountMap[instruction.Accounts[3]]

	default:
		return nil, nil // Skip unsupported instructions
	}

	transfer := &Transfer{
		Type:         instructionTypeToString(instructionType),
		Source:       sourceAccount,
		Destination:  destinationAccount,
		Authority:    authorityAccount,
		TokenAddress: mintAddress,
		Amount:       fmt.Sprintf("%d", amount),
	}

	return transfer, nil
}

func instructionTypeToString(instructionType byte) string {
	switch instructionType {
	case 3:
		return "transfer"
	case 12:
		return "transferChecked"
	default:
		return "unknown"
	}
}

// tryDeriveMintAddress assume source or destination address is ATA address, compare it to derivedATA, if it's match, then return mint address
func tryDeriveMintAddress(source, destination string, authorities map[string]struct{}, mintAddresses map[string]struct{}) string {
	for authority := range authorities {
		for mintAddress, _ := range mintAddresses {
			derivedATA, _ := deriveAssociatedTokenAddress(
				common.PublicKeyFromString(authority),
				common.PublicKeyFromString(mintAddress),
			)
			if derivedATA.ToBase58() == source || derivedATA.ToBase58() == destination {
				return mintAddress
			}
		}
	}
	return ""
}

// deriveAssociatedTokenAddress derives the associated token address for a given owner and mint
func deriveAssociatedTokenAddress(owner, mint common.PublicKey) (common.PublicKey, error) {
	seeds := [][]byte{
		owner.Bytes(),
		common.TokenProgramID.Bytes(),
		mint.Bytes(),
	}

	programDerivedAddress, _, err := common.FindProgramAddress(seeds, common.SPLAssociatedTokenAccountProgramID)
	if err != nil {
		return common.PublicKey{}, fmt.Errorf("failed to find program address: %w", err)
	}

	return programDerivedAddress, nil
}
Shiming Liu

Written by

Shiming Liu

Blockchain developer at Amber Group, MEV searcher⚡︎🤖, ex-developer at IBM, HSBC.