Building Go CLI Tools for Blockchain Wallet and Storage Management
> November 5, 2021
During my time at 0chain, I owned two Go-based CLI tools that developers used to interact with the decentralized storage protocol: zwalletCLI for wallet operations and zboxCLI for storage management. This post shares what I learned about designing ergonomic, reliable CLI tools in Go.
> Why Go for CLI Tools?
Go produces statically linked, cross-platform binaries with zero runtime dependencies — perfect for developer tooling. The standard library has strong support for networking, cryptography, and concurrency, all of which matter when you're talking to a blockchain.
> CLI Structure with Cobra
Both tools used the Cobra library for command structure:
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "zwallet",
Short: "0chain wallet CLI",
}
var createWalletCmd = &cobra.Command{
Use: "create",
Short: "Create a new wallet",
RunE: runCreateWallet,
}
func init() {
rootCmd.AddCommand(createWalletCmd)
createWalletCmd.Flags().StringP("name", "n", "", "wallet name (required)")
createWalletCmd.MarkFlagRequired("name")
}
func main() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}> Talking to the Blockchain
Blockchain calls require signing transactions with the wallet's private key. Here is a simplified version of the signing flow:
import (
"crypto/ed25519"
"encoding/hex"
)
type Transaction struct {
ClientID string `json:"client_id"`
ToClientID string `json:"to_client_id"`
Value int64 `json:"value"`
Hash string `json:"hash"`
Signature string `json:"signature"`
}
func signTransaction(txn *Transaction, privateKey ed25519.PrivateKey) error {
hash := computeHash(txn)
txn.Hash = hash
sig := ed25519.Sign(privateKey, []byte(hash))
txn.Signature = hex.EncodeToString(sig)
return nil
}Every transaction is hashed and signed before being submitted to a consensus node.
> Storage Operations in zboxCLI
zboxCLI handled file uploads, downloads, and allocation management on 0chain's decentralized storage network:
var uploadCmd = &cobra.Command{
Use: "upload",
Short: "Upload a file to 0chain storage",
RunE: func(cmd *cobra.Command, args []string) error {
localPath, _ := cmd.Flags().GetString("localpath")
remotePath, _ := cmd.Flags().GetString("remotepath")
allocationID, _ := cmd.Flags().GetString("allocation")
return uploadFile(allocationID, localPath, remotePath)
},
}
func uploadFile(allocationID, localPath, remotePath string) error {
allocation, err := sdk.GetAllocation(allocationID)
if err != nil {
return fmt.Errorf("get allocation: %w", err)
}
return allocation.UploadFile(localPath, remotePath, nil)
}> Error Handling and User Feedback
Good CLI tools give clear, actionable error messages. We made a rule: never surface raw Go error strings to the user.
func runCreateWallet(cmd *cobra.Command, args []string) error {
name, _ := cmd.Flags().GetString("name")
wallet, err := sdk.CreateWallet(name)
if err != nil {
return fmt.Errorf("failed to create wallet %q: %w", name, err)
}
fmt.Printf("Wallet created successfully.\nAddress: %s\n", wallet.ClientID)
return nil
}We also added a `--json` flag for machine-readable output, enabling scripting and CI integration.
> Testing CLI Commands
Testing CLI tools in Go is straightforward using the Cobra testing pattern:
func TestCreateWalletCommand(t *testing.T) {
cmd := rootCmd
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetArgs([]string{"create", "--name", "test-wallet"})
err := cmd.Execute()
assert.NoError(t, err)
assert.Contains(t, buf.String(), "Wallet created successfully")
}> Lessons Learned
- **Use Cobra from day one** — manual flag parsing becomes unmaintainable fast.
- **Wrap errors with context** — `fmt.Errorf("operation: %w", err)` makes debugging dramatically easier.
- **Separate SDK from CLI** — the blockchain SDK should be a standalone package; the CLI is just one consumer.
- **Provide both human and machine output** — `--json` flags are a small effort with high value for power users.
Maintaining these tools taught me that developer experience is a feature, not an afterthought.