Have you ever wanted to explore blockchain development using Go? Or wanted to build Go applications powered by the Solana network? If you have, this article is for you.
Blockchain is the technology powering cryptocurrencies and decentralized applications. It is still in its early stages and is gaining massive adoption in several industries, leading to new job roles and opportunities. The average blockchain developer earns $154,550 annually, according to ZipRecruiter.
The simplest way to get started with blockchain development is by building a cryptocurrency wallet to store tokens and create transactions. There are several existing blockchains, and one of my favorites is Solana because of its rich ecosystem, speed, and developer friendliness.
In this article, we will learn how to interact with the Solana network using Go and build a crypto wallet to store, receive, and transfer coins from scratch.
To follow and understand this tutorial, you will need the following:
Solana ($SOL) is the fastest globally decentralized blockchain, emphasizing speed, scalability, and user-friendliness. It also has the fastest-growing ecosystem in the crypto space with over 400 projects, including Defi, NFTs, Web3, and many more.
Solana stands out from many other blockchains by offering the following:
A leading cybersecurity firm, Kudelski Security, has audited Solana’s software architecture, and the results are available here. You can also view the Solana network statistics on Solana Beach.
Install the solana-go-sdk package, which is a Go Software Development Kit (SDK) for Solana. It allows Go applications to interact with the Solana network, including using all the methods provided by the Solana JSON RPC API.
Create a new Go project in your text editor or IDE and initialize your go.mod
file. You are free to use any name for your package:
go mod init solana-wallet
Install the solana-go-sdk package in your project. In the terminal, type the following:
go get -u github.com/portto/solana-go-sdk
Import the Solana SDK package into your application, then create an RPC client instance connected to the Solana Mainnet network.
Create a file named main.go
and save the following code in it:
package main import ( "context" "fmt" "github.com/portto/solana-go-sdk/client" "github.com/portto/solana-go-sdk/client/rpc" ) func main() { // create a RPC client c := client.NewClient(rpc.MainnetRPCEndpoint) // get the current running Solana version response, err := c.GetVersion(context.TODO()) if err != nil { panic(err) } fmt.Println("version", response.SolanaCore) }
In this code, we imported the client
and client/rpc
modules from the Solana SDK into the application to create an RPC client and connect to the Solana network.
Then, we used the GetVersion()
method to retrieve the current Solana versions running on the node by supplying a request context using context.TODO()
for the server to accept.
Finally, we checked for errors in the operation using the err
variable returned from GetVersion()
and displayed the network version from the response
variable. If the code runs without any errors, it means you have successfully connected to the Solana Mainnet network with Go.
If Go fails to run the
main.go
file due to"missing go.sum entry"
errors, you need to run thego mod tidy
command in your terminal to fix the missing module entries.
The Solana SDK provides a types.NewAccount()
function that returns a newly generated Solana wallet. Let’s see how to use it:
package main import ( "fmt" "github.com/portto/solana-go-sdk/types" ) func main() { // create a new wallet using types.NewAccount() wallet := types.NewAccount() // display the wallet public and private keys fmt.Println("Wallet Address:", wallet.PublicKey.ToBase58()) fmt.Println("Private Key:", wallet.PrivateKey) }
Here, we used the types.NewAccount()
function in the Solana SDK that generates new wallets, then prints out its wallet address (public key in base58) and private key (byte slice).
The Solana SDK provides three functions to import Solana wallets. Each of them returns a types.Account
object representing a Solana wallet using its private key in different forms (base58, byte slice, and hex value):
// import a wallet with base58 private key types.AccountFromBase58("") // import a wallet with bytes slice private key types.AccountFromBytes([]byte{}) // import a wallet with hex private key types.AccountFromHex("")
Let’s see how to recover a Solana wallet from its private key:
package main import ( "fmt" "github.com/portto/solana-go-sdk/types" ) func main() { // create a new wallet wallet := types.NewAccount() fmt.Println("Wallet Address:", wallet.PublicKey.ToBase58()) // import the wallet using its private key importedWallet, err := types.AccountFromBytes(wallet.PrivateKey) // check for errors if err != nil { panic(err) } // display the imported wallet public and private keys fmt.Println("Imported Wallet Address:", importedWallet.PublicKey.ToBase58()) }
Here, we created a new wallet using the types.NewAcccount()
function, then used the types.AccountFromBytes()
function to import the wallet and compared their addresses. If they are equal, the wallet import is successful.
Because we are creating new wallets, their balances will always be zero. Solana provides a requestAirdrop() method in its JSON RPC API to request coins for development purposes. Let’s see how to use it:
package main import ( "context" "fmt" "github.com/portto/solana-go-sdk/client" "github.com/portto/solana-go-sdk/client/rpc" "github.com/portto/solana-go-sdk/types" ) func main() { // create a RPC client c := client.NewClient(rpc.DevnetRPCEndpoint) // create a new wallet wallet := types.NewAccount() // request for 1 SOL airdrop using RequestAirdrop() txhash, err := c.RequestAirdrop( context.TODO(), // request context wallet.PublicKey.ToBase58(), // wallet address requesting airdrop 1e9, // amount of SOL in lamport ) // check for errors if err != nil { panic(err) } fmt.Println("Transaction Hash:", txhash) }
We used the RequestAirdrop()
function to request Devnet coins for a Solana wallet by providing a request context, wallet public key (in base58), and lamport amount.
The
RequestAirdrop()
function accepts theamount
parameter in lamport, the smallest fractional unit of SOL, similar to satoshi for Bitcoin.1 lamport ~ 0.000000001 SOL.
You can use the Solana Devnet Explorer to track the status of your transactions.
Next, use the GetBalance()
method provided by the Solana SDK to fetch the amount of SOL owned by the Solana wallet from its wallet address. Let’s see how to use it:
package main import ( "context" "fmt" "github.com/portto/solana-go-sdk/client" "github.com/portto/solana-go-sdk/client/rpc" ) func main() { // create a RPC client c := client.NewClient(rpc.DevnetRPCEndpoint) // fetch the balance using GetBalance() balance, err := c.GetBalance( context.TODO(), // request context "8LdDAFdGuvZdhhnheUv9jVtiv9wQT3eTk2E46FodZP38", // wallet to fetch balance for ) // check for errors if err != nil { panic(err) } fmt.Println("Wallet Balance in Lamport:", balance) fmt.Println("Wallet Balance in SOL:", balance/1e9) }
Before performing transfers on Solana, you must follow four steps to ensure your transaction is created on the network.
First, retrieve the most recent block hash from the network using the GetRecentBlockhash()
function:
response, err := c.GetRecentBlockhash(context.TODO())
Then, make a transfer message with the retrieved block hash and public key of the transaction signer:
message := types.NewMessage( wallet.PublicKey, // public key of the transaction signer []types.Instruction{ sysprog.Transfer( wallet.PublicKey, // public key of the transaction sender common.PublicKeyFromString(to), // wallet address of the transaction receiver 1e9, // transaction amount in lamport ), }, response.Blockhash, // recent block hash )
A transfer message contains the public keys of the transaction sender, receiver, and amount. It is also possible to have multiple transfers in a single transaction.
Create a transaction with the transfer message and transaction signer:
tx, err := types.NewTransaction(message, []types.Account{wallet, wallet})
The transaction signer is the account that pays the fee for that transaction. It is possible to let another account pay the transaction fee, but that account must also sign the transaction.
Next, send the transaction to the network, like so:
txhash, err := c.SendTransaction2(context.TODO(), tx)
After sending the transaction to the network, you will receive its hash that we can use to track the transaction. Here’s the complete transfer code:
package main import ( "context" "fmt" "github.com/portto/solana-go-sdk/client" "github.com/portto/solana-go-sdk/client/rpc" "github.com/portto/solana-go-sdk/common" "github.com/portto/solana-go-sdk/program/sysprog" "github.com/portto/solana-go-sdk/types" ) func main() { // create a RPC client c := client.NewClient(rpc.DevnetRPCEndpoint) // import a wallet with Devnet balance wallet, _ := types.AccountFromBytes([]byte{}) // fetch the most recent blockhash response, err := c.GetRecentBlockhash(context.TODO()) if err != nil { panic(err) } // make a transfer message with the latest block hash message := types.NewMessage( wallet.PublicKey, // public key of the transaction signer []types.Instruction{ sysprog.Transfer( wallet.PublicKey, // public key of the transaction sender common.PublicKeyFromString("8t88TuqUxDMVpYGHcVoXnBCAH7TPrdZ7ydr4xqcNu2Ym"), // wallet address of the transaction receiver 1e9, // transaction amount in lamport ), }, response.Blockhash, // recent block hash ) // create a transaction with the message and TX signer tx, err := types.NewTransaction(message, []types.Account{wallet, wallet}) if err != nil { panic(err) } // send the transaction to the blockchain txhash, err := c.SendTransaction2(context.TODO(), tx) if err != nil { panic(err) } fmt.Println("Transaction Hash:", txhash) }
Now that you have learned how to interact with the Solana network, let’s extend the code blocks we created earlier to build a fully functional cryptocurrency wallet using Go.
Let’s start by creating a custom type for the wallet. Save the following code in your main.go
file:
package main import ( "context" "github.com/portto/solana-go-sdk/client" "github.com/portto/solana-go-sdk/common" "github.com/portto/solana-go-sdk/program/sysprog" "github.com/portto/solana-go-sdk/types" ) type Wallet struct { account types.Account c *client.Client }
We need a function to generate new instances of the Wallet
type so we can use it. Add the following code to the main.go
file:
func CreateNewWallet(RPCEndpoint string) Wallet { return Wallet{ types.NewAccount(), client.NewClient(RPCEndpoint), } }
We can create new wallets with our application by adding a function that imports existing Solana wallets using their private key. Add the following code to the main.go
file:
func ImportOldWallet(privateKey []byte, RPCEndpoint string) (Wallet, error) { wallet, err := types.AccountFromBytes(privateKey) if err != nil { return Wallet{}, err } return Wallet{ wallet, client.NewClient(RPCEndpoint), }, nil }
Now we have working code that creates and imports Solana wallet accounts. Let’s create some methods for the Wallet
type.
We’ll begin by adding a function that requests SOL airdrops and retrieves the wallet balance. Add the following code to the main.go
file:
func (w Wallet) RequestAirdrop(amount uint64) (string, error) { // request for SOL using RequestAirdrop() txhash, err := w.c.RequestAirdrop( context.TODO(), // request context w.account.PublicKey.ToBase58(), // wallet address requesting airdrop amount, // amount of SOL in lamport ) if err != nil { return "", err } return txhash, nil } func (w Wallet) GetBalance() (uint64, error) { // fetch the balance using GetBalance() balance, err := w.c.GetBalance( context.TODO(), // request context w.account.PublicKey.ToBase58(), // wallet to fetch balance for ) if err != nil { return 0, nil } return balance, nil }
Next, we’ll create a function to transfer SOL from our wallet to other wallets on the Solana network. Add the following code to the main.go
file:
func (w Wallet) Transfer(receiver string, amount uint64) (string, error) { // fetch the most recent blockhash response, err := w.c.GetRecentBlockhash(context.TODO()) if err != nil { return "", err } // make a transfer message with the latest block hash message := types.NewMessage( w.account.PublicKey, // public key of the transaction signer []types.Instruction{ sysprog.Transfer( w.account.PublicKey, // public key of the transaction sender common.PublicKeyFromString(receiver), // wallet address of the transaction receiver amount, // transaction amount in lamport ), }, response.Blockhash, // recent block hash ) // create a transaction with the message and TX signer tx, err := types.NewTransaction(message, []types.Account{w.account, w.account}) if err != nil { return "", err } // send the transaction to the blockchain txhash, err := w.c.SendTransaction2(context.TODO(), tx) if err != nil { return "", err } return txhash, nil }
Now that we have created all the methods for the Wallet
type. Let’s test it out:
// create a new wallet wallet := CreateNewWallet(rpc.DevnetRPCEndpoint) // request for an airdrop fmt.Println(wallet.RequestAirdrop(1e9)) // make transfer to another wallet fmt.Println(wallet.Transfer("8t88TuqUxDMVpYGHcVoXnBCAH7TPrdZ7ydr4xqcNu2Ym", 5e8)) // fetch wallet balance fmt.Println(wallet.GetBalance())
By exploring the world of blockchain development using Solana and Go, you built a cryptocurrency wallet with minimal effort. We saw how to create and retrieve Solana wallets, store, receive, and transfer coins using the solana-go-sdk
package.
The source code of the Solana wallet is available as a GitHub Gist. Also, you can visit the official documentation for Solana and the solana-go-sdk to explore and build amazing things with your newly acquired knowledge.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app or site. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.