Table of Contents
- RPC methods
- Account
- What is base58?
- Account length
- Default account creation
- Program
- Instruction
- Faucet
- Check Sol Balance
- Transfer
- Rent
- Token
- SPL token
- Create SPL token
- Get mint
- Create token account
- Mint to
- Get token account
- Get token mint from ATA
- Transfer token
- Set token metadata
- Get token metadata
- Get full token info
- Extract token transfer details
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
(excludes0
)
- Uppercase letters:
A-Z
(excludesO
)
- Lowercase letters:
a-z
(excludesl
andI
)
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:
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: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
}
- Core Instructions:
Source code on github https://github.com/solana-labs/solana-program-library/blob/master/token/program/src/instruction.rs
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;
- Account Structures:
- Mint Account
- Token Account
- Associated Token Accounts (ATAs):
- Standard way to derive token accounts for users
- Metadata (via Metaplex):
- While not part of the core Token Program, it's a widely adopted standard.
- Important Public Keys:
- Token Program ID: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
- Associated Token Program ID: ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL
Create SPL token
- Create a mint account, the token owner is set to be the Token Program
TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
- 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.):
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:
View the token page:
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
*/
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
*/
Get token metadata
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
*/
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
andtransferChecked
instructions, both can execute a transfer.
- But for
transfer
instruction, there’s no tokenmint
address in the context, you should derive themint
address, refers totryDeriveMintAddress
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
, andname
).
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
}