Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ go.work.sum
# env file
.env
/data
/release
/tests/system/data
tests/system/**/supernode-data*
26 changes: 25 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
make .PHONY: test-unit test-integration test-system install-lumera setup-supernodes system-test-setup build build-release
.PHONY: build build-release build-sncli
.PHONY: install-lumera setup-supernodes system-test-setup
.PHONY: gen-cascade gen-supernode
.PHONY: test-e2e test-unit test-integration test-system

# Build variables
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
Expand All @@ -15,12 +18,33 @@ build:
CGO_ENABLED=1 \
GOOS=linux \
GOARCH=amd64 \
echo "Building supernode..."
go build \
-trimpath \
-ldflags="-s -w $(LDFLAGS)" \
-o release/supernode-linux-amd64 \
./supernode
@chmod +x release/supernode-linux-amd64
@echo "supernode built successfully at release/supernode-linux-amd64"

build-sncli: release/sncli

release/sncli: $(SNCLI_SRC) cmd/sncli/go.mod cmd/sncli/go.sum
@mkdir -p release
@echo "Building sncli..."
@RELEASE_DIR=$(CURDIR)/release && \
cd cmd/sncli && \
CGO_ENABLED=1 \
GOOS=linux \
GOARCH=amd64 \
go build \
-trimpath \
-ldflags="-s -w $(LDFLAGS)" \
-o $$RELEASE_DIR/sncli && \
chmod +x $$RELEASE_DIR/sncli && \
echo "sncli built successfully at $$RELEASE_DIR/sncli"

SNCLI_SRC=$(shell find cmd/sncli -name "*.go")

test-unit:
go test -v ./...
Expand Down
88 changes: 88 additions & 0 deletions cmd/sncli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# sncli - Supernode CLI Client

`sncli` is a lightweight command-line interface for interacting with a Lumera Supernode over secure gRPC. It supports health checks, service discovery, task registration, and status queries.

---

## 🔧 Build Instructions

To build `sncli` from source:

```bash
make build-sncli
```

> The binary will be located at: `release/sncli`

---

## ⚙️ Configuration

Create a `config.toml` file in the same directory where you run `sncli`:

```toml
# Lumera blockchain connection settings
[lumera]
grpc_addr = "localhost:9090"
chain_id = "lumera-devnet-1"

# Keyring settings for managing keys and identities
[keyring]
backend = "test" # "file", "test", or "os"
dir = "~/.lumera" # Directory where keyring is stored
key_name = "sncli-account" # Name of local key
local_address = "lumera1abc..." # Bech32 address of local account (must exist on-chain)

# Supernode peer information
[supernode]
grpc_endpoint = "127.0.0.1:4444"
address = "lumera1supernodeabc123" # Bech32 address of the Supernode
```

> Ensure the `local_address` exists on-chain (i.e., has received funds or sent a tx).

---

## 🚀 Usage

Run the CLI by calling the built binary with a command:

```bash
./sncli [<options>] <command> [args...]
```

### Supported Command-Line Options

| Option | Description |
| --------------- | ------------------------------------------------------------------------------------------------- |
| --config | Path to config file. Supports ~. Default: ./config.toml or SNCLI_CONFIG_PATH environment variable |
| --grpc_endpoint | Override gRPC endpoint for Supernode (e.g., 127.0.0.1:4444) |
| --address | Override Supernode's Lumera address |

### Supported Commands

| Command | Description |
| ---------------------- | -------------------------------------------------------- |
| `help` | Show usage instructions |
| `list` | List available gRPC services from the Supernode |
| `list` `<service>` | List methods in a specific gRPC service |
| `health-check` | Check if the Supernode is alive |
| `get-status` | Query current CPU/memory usage reported by the Supernode |

### Example

```bash
./sncli --config ~/.sncli.toml --grpc_endpoint 10.0.0.1:4444 --address lumera1xyzabc get-status
```

---

## 📝 Notes

- `sncli` uses a secure gRPC connection with a handshake based on the Lumera keyring.
- The Supernode address must match a known peer on the network.
- Make sure`sncli-account` has been initialized and exists on the chain.
- Config file path is resolved using this algorithm:
- First uses --config flag (if provided)
- Else uses SNCLI_CONFIG_PATH environment variable (if defined)
- Else defaults to ./config.toml
185 changes: 185 additions & 0 deletions cmd/sncli/cli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package cli

import (
"context"
"log"
"os"
"path/filepath"
"strings"

"github.com/BurntSushi/toml"
"github.com/spf13/pflag"

"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/LumeraProtocol/supernode/pkg/net/credentials/alts/conn"
"github.com/LumeraProtocol/supernode/sdk/adapters/lumera"
snkeyring "github.com/LumeraProtocol/supernode/pkg/keyring"
sdkcfg "github.com/LumeraProtocol/supernode/sdk/config"
sdklog "github.com/LumeraProtocol/supernode/sdk/log"
sdknet "github.com/LumeraProtocol/supernode/sdk/net"
)

const (
defaultConfigFileName = "config.toml"
)

type CLI struct {
opts CLIOptions
cfg *CLIConfig
kr keyring.Keyring
sdkConfig sdkcfg.Config
lumeraClient lumera.Client
snClient sdknet.SupernodeClient
}

func (c *CLI) parseCLIOptions() {
pflag.StringVar(&c.opts.ConfigPath, "config", "", "Path to config file")
pflag.StringVar(&c.opts.GrpcEndpoint, "grpc_endpoint", "", "Supernode gRPC endpoint")
pflag.StringVar(&c.opts.SupernodeAddr, "address", "", "Supernode Lumera address")
pflag.Parse()

args := pflag.Args()
if len(args) > 0 {
c.opts.Command = args[0]
c.opts.CommandArgs = args[1:]
}
}

func NewCLI() *CLI {
cli := &CLI{}
return cli
}

func processConfigPath(path string) string {
// expand environment variables if any
path = os.ExpandEnv(path)
// replaces ~ with the user's home directory
if strings.HasPrefix(path, "~") {
home, err := os.UserHomeDir()
if err != nil {
log.Fatalf("Unable to resolve home directory: %v", err)
}
path = filepath.Join(home, path[1:])
}
// check if path defines directory
if info, err := os.Stat(path); err == nil && info.IsDir() {
path = filepath.Join(path, defaultConfigFileName)
}
path = filepath.Clean(path)
return path
}

// detectConfigPath resolves the configuration file path based on:
// 1. CLI argument (--config) if provided
// 2. SNCLI_CONFIG_PATH environment variable
// 3. Default to ./config.toml
func (c *CLI) detectConfigPath() string {
if c.opts.ConfigPath != "" {
return processConfigPath(c.opts.ConfigPath)
}
if envPath := os.Getenv("SNCLI_CONFIG_PATH"); envPath != "" {
return processConfigPath(envPath)
}
return defaultConfigFileName
}

func (c *CLI) loadCLIConfig() {
path := c.detectConfigPath()
_, err := toml.DecodeFile(path, &c.cfg)
if err != nil {
log.Fatalf("Failed to load config from %s: %v", path, err)
}
}

func (c *CLI) validateCLIConfig() {
if c.opts.GrpcEndpoint != "" {
c.cfg.Supernode.GRPCEndpoint = c.opts.GrpcEndpoint
}
if c.opts.SupernodeAddr != "" {
c.cfg.Supernode.Address = c.opts.SupernodeAddr
}
}

func (c *CLI) Initialize() {
// Parse command-line options
c.parseCLIOptions()
// Load options from toml configuration file
c.loadCLIConfig()
// Validate configuration & override with CLI options if provided
c.validateCLIConfig()

// Initialize Supernode SDK
snkeyring.InitSDKConfig()

// Initialize keyring
var err error
c.kr, err = snkeyring.InitKeyring(c.cfg.Keyring.Backend, c.cfg.Keyring.Dir)
if err != nil {
log.Fatalf("Keyring init failed: %v", err)
}

// Create Lumera client adapter
c.sdkConfig = sdkcfg.NewConfig(
sdkcfg.AccountConfig{
LocalCosmosAddress: c.cfg.Keyring.LocalAddress,
KeyName: c.cfg.Keyring.KeyName,
Keyring: c.kr,
},
sdkcfg.LumeraConfig{
GRPCAddr: c.cfg.Lumera.GRPCAddr,
ChainID: c.cfg.Lumera.ChainID,
},
)

c.lumeraClient, err = lumera.NewAdapter(context.Background(), lumera.ConfigParams{
GRPCAddr: c.sdkConfig.Lumera.GRPCAddr,
ChainID: c.sdkConfig.Lumera.ChainID,
KeyName: c.sdkConfig.Account.KeyName,
Keyring: c.kr,
}, sdklog.NewNoopLogger())
if err != nil {
log.Fatalf("Lumera client init failed: %v", err)
}

conn.RegisterALTSRecordProtocols()
}

func (c *CLI) Finalize() {
conn.UnregisterALTSRecordProtocols()

if c.snClient != nil {
c.snClient.Close(context.Background())
}
}

func (c *CLI) snClientInit() {
if c.snClient != nil {
return // Already initialized
}

if c.cfg.Supernode.Address == "" || c.cfg.Supernode.GRPCEndpoint == "" {
log.Fatal("Supernode address and gRPC endpoint must be configured")
}

supernode := lumera.Supernode{
CosmosAddress: c.cfg.Supernode.Address,
GrpcEndpoint: c.cfg.Supernode.GRPCEndpoint,
}

clientFactory := sdknet.NewClientFactory(
context.Background(),
sdklog.NewNoopLogger(),
c.kr,
c.lumeraClient,
sdknet.FactoryConfig{
LocalCosmosAddress: c.cfg.Keyring.LocalAddress,
PeerType: 1, // Simplenode
},
)

var err error
c.snClient, err = clientFactory.CreateClient(context.Background(), supernode)
if err != nil {
log.Fatalf("Supernode client init failed: %v", err)
}
}
47 changes: 47 additions & 0 deletions cmd/sncli/cli/dispatch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cli

import (
"fmt"
"log"
)

func showHelp() {
helpText := `Supernode CLI Usage:
./sncli [options] <command> [args...]

Available Options:
--config <path> Path to config file (default: ./config.toml or SNCLI_CONFIG_PATH env)
--grpc_endpoint <addr> Override Supernode gRPC endpoint (e.g., localhost:9090)
--address <addr> Override Supernode Lumera address

Available Commands:
help Show this help message
list List available gRPC services on Supernode
list <service> List methods in a specific gRPC service
health-check Check Supernode health status
get-status Query Supernode's current status (CPU, memory)`
fmt.Println(helpText)
}

func (c *CLI) Run() {
// Dispatch command handler
switch c.opts.Command {
case "help", "":
showHelp()
case "list":
if err := c.listGRPCMethods(); err != nil {
log.Fatalf("List gRPC methods failed: %v", err)
}
case "health-check":
if err := c.healthCheck(); err != nil {
log.Fatalf("Health check failed: %v", err)
}
case "get-status":
if err := c.getSupernodeStatus(); err != nil {
log.Fatalf("Get supernode status failed: %v", err)
}
default:
log.Fatalf("Unknown command: %s", c.opts.Command)
}
}

Loading