Skip to content

Latest commit

 

History

History
290 lines (232 loc) · 14.6 KB

File metadata and controls

290 lines (232 loc) · 14.6 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

chiliz-cli is a hybrid CLI+TUI application for managing multi-chain EVM transactions. Built with Cobra (CLI), Bubble Tea (TUI), and go-ethereum, it provides both scriptable CLI commands and an interactive keyboard-driven interface for balance checks, transaction queries, and token transfers across EVM-compatible chains.

Currently configured for Chiliz Chain with PEPPER token. New chains/tokens are added via JSON config files with zero code changes.

Distributed via Homebrew (brew tap GrapeInTheTree/tap && brew install chiliz), go install, and GitHub Releases.

Development Commands

# Run (TUI mode)
go run ./cmd/chiliz

# Run (CLI mode)
go run ./cmd/chiliz address 0x1234...
go run ./cmd/chiliz tx 0xabcd... --json
go run ./cmd/chiliz block latest
go run ./cmd/chiliz chain-info --json
go run ./cmd/chiliz call 0x1234... "totalSupply()(uint256)"

# Build
go build -o chiliz ./cmd/chiliz

# Test
go test ./...

# Lint
go vet ./...
golangci-lint run

# Logs (TUI mode writes to chiliz.log)
tail -f chiliz.log

# Regenerate ERC-20 bindings after ABI changes
abigen --abi internal/infra/ethereum/abi/erc20.json \
       --pkg contracts --type ERC20 \
       --out internal/infra/ethereum/contracts/erc20.go

# Release (tag triggers GoReleaser via GitHub Actions)
git tag v0.3.0
git push --tags

CLI Commands

chiliz                              # Interactive TUI mode (no args)
chiliz address <addr>               # Address info: balance, nonce, tx history, tokens
chiliz tx <hash>                    # Transaction details with receipt
chiliz block [number|latest]        # Block information
chiliz chain-info                   # Chain status: latest block, gas price
chiliz call <contract> <sig> [args] # Generic read-only contract call (eth_call)
chiliz validators                   # Chiliz validator set + staking status
chiliz staking <addr>               # Personal staking positions + rewards
chiliz token <contract>             # Token metadata via Chiliscan API
chiliz contract <addr>              # Contract source, compiler, deployer
chiliz holders <token>              # Top token holders + count
chiliz logs --address --event       # On-chain event logs (eth_getLogs)
chiliz rpc <method> [params]        # Raw JSON-RPC escape hatch
chiliz init                         # Initialize config in ~/.chiliz/
chiliz version                      # Build version and commit hash

Global flags:
  --chain <name>     Blockchain network (default: first in chains.json)
  --json             Machine-readable JSON output
  --config <path>    Config directory path

Architecture

Three-layer clean architecture under internal/, with Cobra CLI routing on top:

cmd/chiliz/
  main.go                           Entry point: Cobra Execute()
  cmd/
    root.go                         Cobra root command + global flags + PersistentPreRunE
                                    Resolves config, chain, and explorer client
                                    appCtx struct holds shared state for all subcommands
                                    slog silenced in CLI mode (TUI sets up file logger)
    tui.go                          TUI launcher (no-args fallback)
                                    Logging to chiliz.log with 0600 permissions
    address.go                      `chiliz address <addr>` — parallel RPC + Explorer fetch
                                    5 concurrent goroutines: balance, nonce, code, txlist, tokens
    tx.go                           `chiliz tx <hash>` — tx + receipt lookup
                                    Derives sender via LatestSignerForChainID
                                    Gets block timestamp for time display
    block.go                        `chiliz block [number]` — block by number
                                    Supports "latest" keyword or specific block number
    chaininfo.go                    `chiliz chain-info` — chain status
                                    Parallel: block number + gas price
    call.go                         `chiliz call <contract> <sig> [args]` — generic eth_call
                                    Uses abi_helper.go for encoding/decoding
                                    Supports cast-style sig: "name(inputs)(outputs)"
    validators.go                   `chiliz validators` — Chiliz staking + governance query
                                    getValidators() + parallel getValidatorStatus() per validator
                                    Voting Power from Governance(0x7002), APY estimated on-chain
                                    Semaphore (max 4 concurrent) + retry for RPC rate limits
    init.go                         `chiliz init` — creates ~/.chiliz/ with default config
                                    Chiliz Mainnet + Spicy Testnet pre-configured
    version.go                      `chiliz version` — ldflags-injected version/commit

internal/
  domain/
    models.go                       Pure domain structs (Chain, Token, Wallet, Contact)
                                    Chain.ExplorerAPIURL for per-chain explorer API
                                    Token.IsNative() detects native vs ERC-20
    output.go                       CLI output types: AddressInfo, TxDetail, BlockInfo,
                                    ChainStatus, TokenBalance, TxSummary
                                    All JSON-serializable with stable field names
                                    Numeric values that exceed JS Number.MAX_SAFE_INTEGER are strings

  infra/
    config/config.go                Loads chains.json, tokens.json, contacts.json, .env
                                    ResolveConfigDir() cascade: --config > env > ~/.chiliz/ > CWD
                                    configPath() joins configDir + filename (empty configDir = relative)
                                    GetPrivateKey() reads key from env only at signing time
                                    ResolveAddress() resolves contact names or validates 0x addresses
                                    Wallets hardcoded: Main Wallet + Test Wallet

    ethereum/
      client.go                     RPC queries (each function creates/closes its own ethclient):
                                    GetBalance, GetNonce, GetCode, GetChainID,
                                    GetGasPrice, GetLatestBlockNumber, GetTransaction,
                                    GetTransactionReceipt, GetBlock, CallContract,
                                    SendTransaction (EIP-1559),
                                    FormatBalance, ParseAmount, GetAddressFromPrivateKey
      abi_helper.go                 Dynamic ABI encoding/decoding without abigen:
                                    ParseCallSignature — splits "name(in)(out)" via paren depth
                                    BuildCalldata — ParseSelector + Pack → 4byte selector + args
                                    ConvertArg — CLI string → Go type (address, uint, bool, etc.)
                                    DecodeOutputs — Unpack + FormatValue → human strings
                                    Reusable for validators, staking, logs commands
      erc20.go                      ERC-20: GetTokenBalance, SendTokenTransaction,
                                    FormatTokenBalance, ParseTokenAmount
                                    pow10() uses big.Int (safe for any decimal count)
      abi/erc20.json                Standard ERC-20 ABI
      contracts/erc20.go            Auto-generated Go bindings (abigen). Do not edit.

    explorer/
      etherscan.go                  Chiliscan/Etherscan-compatible API client (Routescan)
                                    GetTxList, GetTokenBalances, GetTokenTxList
                                    Rate-limited (2 req/sec via time.Tick), no API key needed
                                    Base URL per chain via Chain.ExplorerAPIURL
                                    Graceful degradation: "No transactions found" is not an error
                                    Internal types (rawTxListEntry) → domain types conversion

  output/
    formatter.go                    Print(jsonMode, v) — type switch for human/JSON output
                                    Human: fmt.Fprintf formatted tables to stdout
                                    JSON: json.Encoder with indent to stdout
                                    relativeTime() for "4d ago" style timestamps
                                    shortenHash() for "0xabcd...ef12" display

  tui/
    app.go                          Router model: manages currentPage, holds shared data
                                    (wallets, chains, tokens, contacts), routes messages
                                    to active page sub-model
                                    Init() loads config asynchronously via tea.Cmd
    style/style.go                  Shared Lipgloss styles (Title, Selected, Error, Success, etc.)
    pages/
      mainmenu/model.go             Menu: Send Transaction, Check Balance, Exit
      balance/model.go              Balance check flow (4-state machine):
                                    Wallet -> Chain -> Token -> fetch balance -> result
      send/model.go                 Send flow (8-state machine):
                                    Wallet -> Chain -> Token -> Recipient -> Amount
                                    -> Confirm -> send tx -> result

Key Design Patterns

Hybrid CLI+TUI: Cobra routes commands. No args = TUI (Bubbletea). Subcommands = CLI with stdout output. Single binary, same infra layer shared.

Dual data sources: RPC for real-time chain state (balance, nonce, blocks, tx by hash). Explorer API (Chiliscan/Routescan) for indexed data (tx history by address, token discovery). Explorer is optional — graceful degradation if unavailable.

Nested Models (TUI): Each TUI page is a self-contained Bubble Tea model with its own Init, Update, View. The router (app.go) delegates to the active page.

Type-safe contract bindings: ERC-20 interactions use abigen-generated Go code, not manual ABI encoding.

Dynamic ABI encoding: chiliz call uses abi.ParseSelector + abi.Arguments.Pack/Unpack for runtime encoding without JSON ABI files. The abi_helper.go module is designed for reuse by future commands (validators, staking, logs).

Config-driven extensibility: Chains, tokens, and contacts are pure JSON. Both TUI and CLI dynamically use these at startup.

Async blockchain calls: RPC operations run as concurrent goroutines (CLI uses sync.WaitGroup, TUI uses Bubble Tea commands).

Package-level appContext: CLI commands share resolved config/chain/explorer via a package-level struct in root.go. Appropriate for a CLI (single execution path, no concurrency at command level).

Data Source Strategy

Data Source Notes
Native balance RPC eth_getBalance
ERC-20 balance RPC eth_call (balanceOf) Known tokens only
Nonce, code, gas price RPC
Tx by hash, receipt RPC
Block by number RPC
Tx history by address Explorer API RPC cannot do this — no eth_getTransactionsByAddress exists
All token holdings Explorer API Token discovery requires indexer
Arbitrary contract reads RPC eth_call via chiliz call with dynamic ABI encoding

Config Path Resolution

  1. --config /path/to/dir flag
  2. CHILIZ_CONFIG_DIR environment variable
  3. ~/.chiliz/ if chains.json exists there
  4. Current working directory (default, backward compatible)

Navigation & Input (TUI)

  • up/k, down/j: cursor movement
  • enter: confirm selection
  • esc: back to main menu
  • ctrl+c: quit
  • Amount entry: 0-9, ., backspace

Extending the App

No code changes needed

What File
Add EVM chain chains.json (include explorer_api_url for tx history support)
Add ERC-20 token tokens.json (decimals must match contract)
Add contact contacts.json

Code changes needed

What Where
Add CLI command Create cmd/chiliz/cmd/<name>.go, register in root.go init()
Add wallet internal/infra/config/config.go LoadWallets() + .env
Add TUI page Create internal/tui/pages/<name>/model.go, register in app.go
Add blockchain query Add to internal/infra/ethereum/client.go
Add explorer query Add to internal/infra/explorer/etherscan.go
Add output type Add struct to internal/domain/output.go, add case in internal/output/formatter.go
Add contract type Place ABI in abi/, run abigen, use bindings in new internal/infra/ethereum/<name>.go

Release Process

Releases are automated via GoReleaser + GitHub Actions.

git tag v0.3.0
git push --tags
# GitHub Actions builds linux/darwin x amd64/arm64, creates GitHub Release,
# and pushes Homebrew formula to GrapeInTheTree/homebrew-tap

Config files:

  • .goreleaser.yml — build matrix, archive format, Homebrew tap config
  • .github/workflows/release.yml — triggered on v* tag push
  • Secrets required: HOMEBREW_TAP_TOKEN (fine-grained PAT with repo scope on homebrew-tap)

Install methods after release:

  • brew tap GrapeInTheTree/tap && brew install chiliz
  • go install github.com/GrapeInTheTree/chiliz-cli/cmd/chiliz@latest
  • Download binary from GitHub Releases page

Key Dependencies

  • cobra v1.10.2 - CLI framework
  • bubbletea v1.3.10 - TUI framework
  • lipgloss v1.1.0 - TUI styling
  • go-ethereum v1.16.7 - Ethereum client, signing, ABI bindings
  • godotenv v1.5.1 - .env loader

Security

Private keys live only in .env (gitignored). They are loaded via config.GetPrivateKey(envKey) at the moment of transaction signing and never cached. The .env.example template shows the expected variable names. CLI read-only commands never access private keys.

Git history has been audited: no secrets have ever been committed.

Token Handling Notes

  • Native vs ERC-20 detection: token.IsNative() checks if address is empty/zero
  • Native transfers use fixed 21000 gas; ERC-20 transfers use EstimateGas() + 20% buffer
  • All gas pricing is EIP-1559 (SuggestGasTipCap + SuggestGasPrice with 1.2x buffer)
  • Decimals are dynamic per token (18 for most, 6 for USDC/USDT, 8 for WBTC)
  • pow10() uses big.Int — safe for any decimal count (no int64 overflow)

Chiliscan API

  • Base URL: https://api.routescan.io/v2/network/mainnet/evm/88888/etherscan/api
  • Free tier: no API key, 2 req/sec, 10,000 calls/day
  • Etherscan-compatible format (module/action query params)
  • Set per chain via explorer_api_url in chains.json
  • Confirmed working endpoints: txlist, addresstokenbalance, tokentx, balance, gasoracle