USDT transaction on Solana network
Introduction
In this small receipt I’m going to show you how to make a USDT transfer on the Solana network.
Requirements
- I’m going to use Golang as the language.
Code with comments
Let’s first create a client for the wallet and import the required libraries.
package main
import (
"context"
"errors"
"github.com/portto/solana-go-sdk/client"
"github.com/portto/solana-go-sdk/common"
"github.com/portto/solana-go-sdk/program/associated_token_account"
"github.com/portto/solana-go-sdk/program/memo"
"github.com/portto/solana-go-sdk/program/token"
"github.com/portto/solana-go-sdk/rpc"
"github.com/portto/solana-go-sdk/types"
)
var (
ErrInsuficientBalance = errors.New("insufficient balance")
)
const (
USDTTokenPublicAddress = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
USDTTokenDecimals uint8 = 6
)
type Client struct {
client *client.Client
wallet types.Account
}
// NewClient ...
func NewClient(ctx context.Context, privateKey string) (*Client, error) {
c := client.NewClient(rpc.DevnetRPCEndpoint)
wallet, err := types.AccountFromBase58(privateKey)
if err != nil {
return nil, err
}
return &Client{
client: c,
wallet: wallet,
}, nil
}
I’ll create two auxiliar methods for retrieving the USDT account associated with a public key.
And retrieving the USDT PublicKey for our wallet. Is important to notice that is not the same the PublicKey, than USDT PublicKey.
Let’s call them GetUSDTAccount
and GetUSDTPublic
.
func (c *Client) GetUSDTAccount(ctx context.Context) (token.TokenAccount, error) {
publicAddress := c.wallet.PublicKey.ToBase58()
mapAccs, err := c.client.GetTokenAccountsByOwner(ctx, publicAddress)
if err != nil {
return token.TokenAccount{}, err
}
for _, acc := range mapAccs {
if acc.Mint.ToBase58() == USDTTokenPublicAddress && acc.Owner.ToBase58() == publicAddress {
return acc, nil
}
}
return token.TokenAccount{}, nil
}
func (c *Client) GetUSDTPublic(ctx context.Context) (common.PublicKey, error) {
publicAddress := c.wallet.PublicKey.ToBase58()
mapAccs, err := c.client.GetTokenAccountsByOwner(ctx, publicAddress)
if err != nil {
return common.PublicKey{}, err
}
for key, acc := range mapAccs {
if acc.Mint.ToBase58() == USDTTokenPublicAddress && acc.Owner.ToBase58() == publicAddress {
return key, nil
}
}
return common.PublicKey{}, nil
}
I’ll also will need to check the available balance in USDT, expressed as 1e6 units of Lamports.
func (c *Client) GetUSDTBalanceLamports(ctx context.Context) (uint64, error) {
usdtPublicAddress, err := c.GetUSDTPublic(ctx)
if err != nil {
return 0, err
}
lamports, _, err := c.client.GetTokenAccountBalance(ctx, usdtPublicAddress.ToBase58())
return lamports, err
}
I’ll also need to get the associated token address for the receiver wallet. So let’s implement a method
for GetAssociatedTokenAddress
.
func (c *Client) GetAssociatedTokenAddress(ctx context.Context, address string) (common.PublicKey, error) {
pubAddress := common.PublicKeyFromString(address)
mintAddress := common.PublicKeyFromString(USDTTokenPublicAddress)
ata, _, err := common.FindAssociatedTokenAddress(pubAddress, mintAddress)
return ata, err
}
Now let’s implement a method for making the transaction of USDTs. It’s important to keep in mind that USDT on Solana, is just a SPL token.
// TransferUSDT make transaction of usdt to solana wallet specified.
// walletAddress: the public address where you want make the usdt transaction.
// amount: amount of USDT to be transfered. The ammount are expressed in 1e6, meaning that 1 USDT is expressed as 1e6.
// memoStr: in case we want to send a message on the transaction, Solana network allow you to do that. This argument
// is for that case.
// return: <string>, <error> we will return the transaction ID to later check it on Solana scanner.
func (c *Client) TransferUSDT(
ctx context.Context,
walletAddress string,
amount uint64,
memoStr string,
) (string, error) {
// we need to get the latest blockhash.
res, err := c.client.GetLatestBlockhash(ctx)
if err != nil {
return "", err
}
usdtTokenAccount, err := c.GetUSDTAccount(ctx)
if err != nil {
return "", err
}
usdtBalance, err := c.GetUSDTBalanceLamports(ctx)
if err != nil {
return "", err
}
// check if our available balance in USDT is enough to make the transaction.
if usdtBalance <= amount {
return "", ErrInsuficientBalance
}
// our usdt public address.
usdtPubAddress, err := c.GetUSDTPublic(ctx)
if err != nil {
return "", err
}
// the token address of the receiver.
receiverAddress, err := c.GetAssociatedTokenAddress(ctx, walletAddress)
if err != nil {
return "", err
}
// let's create the intructions to be executed.
// for a more detailed explanation feel free to check the official doc in this link
// https://docs.solana.com/es/developing/programming-model/transactions#overview-of-a-transaction
instructions := make([]types.Instruction, 0)
// could be the case that the account we are trying to send the usdt
// doesn't have a token account. In this intruction I specified, that if that's the case
// I'll pay for the creation of this account, which is cheap, but the owner will be the other
// part. In our case the receiver.
_, err = c.client.GetTokenAccount(ctx, receiverAddress.ToBase58())
if err != nil {
// add intruction for creating token account.
instructions = append(instructions, associated_token_account.CreateAssociatedTokenAccount(associated_token_account.CreateAssociatedTokenAccountParam{
Funder: c.wallet.PublicKey,
Owner: common.PublicKeyFromString(walletAddress),
Mint: usdtTokenAccount.Mint,
AssociatedTokenAccount: receiverAddress,
}))
}
// intruction associated with the transaction, where we specify everything needed.
instructions = append(instructions, token.TransferChecked(token.TransferCheckedParam{
From: usdtPubAddress, // from (should be a token account)
To: receiverAddress, // from (should be a token account)
Mint: usdtTokenAccount.Mint, // mint
Auth: usdtTokenAccount.Owner, // from's owner
Signers: []common.PublicKey{},
Amount: amount,
Decimals: USDTTokenDecimals, // in our case usdt decimals is 6.
}))
// if you pass an empty string we won't include
// the intruction associated with the comment.
if memoStr != "" {
instructions = append(instructions, memo.BuildMemo(memo.BuildMemoParam{
SignerPubkeys: []common.PublicKey{c.wallet.PublicKey},
Memo: []byte(memoStr),
}))
}
tx, err := types.NewTransaction(types.NewTransactionParam{
Message: types.NewMessage(types.NewMessageParam{
FeePayer: c.wallet.PublicKey,
RecentBlockhash: res.Blockhash, // here we use the recent blockhash.
Instructions: instructions, // including our previously constructed intructions.
}),
Signers: []types.Account{
c.wallet,
c.wallet,
},
})
if err != nil {
return "", err
}
// send the transaction.
txnHash, err := c.client.SendTransaction(ctx, tx)
if err != nil {
return "", err
}
// transaction hash to check it on https://explorer.solana.com/
return txnHash, nil
}
That’s all, feel free to join all the pieces yourself :).