~/krishna_dhakal
#Go#CLI#Blockchain#DevTools

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


  1. **Use Cobra from day one** — manual flag parsing becomes unmaintainable fast.
  2. **Wrap errors with context** — `fmt.Errorf("operation: %w", err)` makes debugging dramatically easier.
  3. **Separate SDK from CLI** — the blockchain SDK should be a standalone package; the CLI is just one consumer.
  4. **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.