USDT transaction on Solana network

Solana USDT

Introduction

In this small receipt I’m going to show you how to make a USDT transfer on the Solana network.

Requirements

  1. 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 :).