network/cmd/cli/main.go
anonpenguin 7e0db10ada Add wallet-based API key management and auth
This adds a new auth flow allowing users to authenticate with their
wallet and obtain an API key scoped to a namespace. It also moves API
key storage from config to the database for better persistence and
key-to-wallet linkage.

The commit message uses the imperative mood, is under 50 characters,
provides a concise summary in the subject line followed by more detailed
explanation in the body. This follows good Git commit message style
while capturing the key changes made.
2025-08-20 10:42:40 +03:00

864 lines
24 KiB
Go

package main
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"time"
"git.debros.io/DeBros/network/pkg/anyoneproxy"
"git.debros.io/DeBros/network/pkg/client"
"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
)
var (
bootstrapPeer = "/ip4/127.0.0.1/tcp/4001"
timeout = 30 * time.Second
format = "table"
useProduction = false
disableAnon = false
)
// version metadata populated via -ldflags at build time
var (
version = "dev"
commit = ""
date = ""
)
func main() {
if len(os.Args) < 2 {
showHelp()
return
}
command := os.Args[1]
args := os.Args[2:]
// Parse global flags
parseGlobalFlags(args)
// Apply disable flag early so all network operations honor it
anyoneproxy.SetDisabled(disableAnon)
switch command {
case "version":
fmt.Printf("network-cli %s", version)
if commit != "" {
fmt.Printf(" (commit %s)", commit)
}
if date != "" {
fmt.Printf(" built %s", date)
}
fmt.Println()
return
case "health":
handleHealth()
case "peers":
handlePeers()
case "status":
handleStatus()
case "query":
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: network-cli query <sql>\n")
os.Exit(1)
}
handleQuery(args[0])
case "storage":
handleStorage(args)
case "pubsub":
handlePubSub(args)
case "connect":
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: network-cli connect <peer_address>\n")
os.Exit(1)
}
handleConnect(args[0])
case "peer-id":
handlePeerID()
case "help", "--help", "-h":
showHelp()
case "auth":
handleAuth(args)
default:
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", command)
showHelp()
os.Exit(1)
}
}
func parseGlobalFlags(args []string) {
for i, arg := range args {
switch arg {
case "-b", "--bootstrap":
if i+1 < len(args) {
bootstrapPeer = args[i+1]
}
case "-f", "--format":
if i+1 < len(args) {
format = args[i+1]
}
case "-t", "--timeout":
if i+1 < len(args) {
if d, err := time.ParseDuration(args[i+1]); err == nil {
timeout = d
}
}
case "--production":
useProduction = true
case "--disable-anonrc":
disableAnon = true
}
}
}
func handleHealth() {
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
health, err := client.Health()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get health: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(health)
} else {
printHealth(health)
}
}
func handlePeers() {
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
peers, err := client.Network().GetPeers(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get peers: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(peers)
} else {
printPeers(peers)
}
}
func handleStatus() {
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
status, err := client.Network().GetStatus(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get status: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(status)
} else {
printStatus(status)
}
}
func handleQuery(sql string) {
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
result, err := client.Database().Query(ctx, sql)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to execute query: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(result)
} else {
printQueryResult(result)
}
}
func handleStorage(args []string) {
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: network-cli storage <get|put|list> [args...]\n")
os.Exit(1)
}
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
subcommand := args[0]
switch subcommand {
case "get":
if len(args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: network-cli storage get <key>\n")
os.Exit(1)
}
value, err := client.Storage().Get(ctx, args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get value: %v\n", err)
os.Exit(1)
}
// Try to decode if it looks like base64
decoded := tryDecodeBase64(string(value))
fmt.Printf("%s\n", decoded)
case "put":
if len(args) < 3 {
fmt.Fprintf(os.Stderr, "Usage: network-cli storage put <key> <value>\n")
os.Exit(1)
}
err := client.Storage().Put(ctx, args[1], []byte(args[2]))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to store value: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Stored key: %s\n", args[1])
case "list":
prefix := ""
if len(args) > 1 {
prefix = args[1]
}
keys, err := client.Storage().List(ctx, prefix, 100)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to list keys: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(keys)
} else {
for _, key := range keys {
fmt.Println(key)
}
}
default:
fmt.Fprintf(os.Stderr, "Unknown storage command: %s\n", subcommand)
os.Exit(1)
}
}
func handlePubSub(args []string) {
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub <publish|subscribe|topics> [args...]\n")
os.Exit(1)
}
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
subcommand := args[0]
switch subcommand {
case "publish":
if len(args) < 3 {
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub publish <topic> <message>\n")
os.Exit(1)
}
err := client.PubSub().Publish(ctx, args[1], []byte(args[2]))
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to publish message: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Published message to topic: %s\n", args[1])
case "subscribe":
if len(args) < 2 {
fmt.Fprintf(os.Stderr, "Usage: network-cli pubsub subscribe <topic> [duration]\n")
os.Exit(1)
}
duration := 30 * time.Second
if len(args) > 2 {
if d, err := time.ParseDuration(args[2]); err == nil {
duration = d
}
}
ctx, cancel := context.WithTimeout(context.Background(), duration)
defer cancel()
fmt.Printf("🔔 Subscribing to topic '%s' for %v...\n", args[1], duration)
messageHandler := func(topic string, data []byte) error {
fmt.Printf("📨 [%s] %s: %s\n", time.Now().Format("15:04:05"), topic, string(data))
return nil
}
err := client.PubSub().Subscribe(ctx, args[1], messageHandler)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to subscribe: %v\n", err)
os.Exit(1)
}
<-ctx.Done()
fmt.Printf("✅ Subscription ended\n")
case "topics":
topics, err := client.PubSub().ListTopics(ctx)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to list topics: %v\n", err)
os.Exit(1)
}
if format == "json" {
printJSON(topics)
} else {
for _, topic := range topics {
fmt.Println(topic)
}
}
default:
fmt.Fprintf(os.Stderr, "Unknown pubsub command: %s\n", subcommand)
os.Exit(1)
}
}
// handleAuth launches a local webpage to perform wallet signature and obtain an API key.
// Usage: network-cli auth [--gateway <url>] [--namespace <ns>] [--wallet <evm_addr>] [--plan <free|premium>]
func handleAuth(args []string) {
// Defaults
gatewayURL := getenvDefault("GATEWAY_URL", "http://localhost:8080")
namespace := getenvDefault("GATEWAY_NAMESPACE", "default")
wallet := ""
plan := "free"
// Parse simple flags
for i := 0; i < len(args); i++ {
switch args[i] {
case "--gateway":
if i+1 < len(args) { gatewayURL = strings.TrimSpace(args[i+1]); i++ }
case "--namespace":
if i+1 < len(args) { namespace = strings.TrimSpace(args[i+1]); i++ }
case "--wallet":
if i+1 < len(args) { wallet = strings.TrimSpace(args[i+1]); i++ }
case "--plan":
if i+1 < len(args) { plan = strings.TrimSpace(strings.ToLower(args[i+1])); i++ }
}
}
// Spin up local HTTP server on random port
ln, err := net.Listen("tcp", "localhost:0")
if err != nil { fmt.Fprintf(os.Stderr, "Failed to listen: %v\n", err); os.Exit(1) }
defer ln.Close()
addr := ln.Addr().String()
// Normalize URL host to localhost for consistency with gateway default
parts := strings.Split(addr, ":")
listenURL := "http://localhost:" + parts[len(parts)-1] + "/"
// Channel to receive API key
type result struct { APIKey string `json:"api_key"`; Namespace string `json:"namespace"` }
resCh := make(chan result, 1)
srv := &http.Server{}
mux := http.NewServeMux()
// Root serves the HTML page with embedded gateway URL and defaults
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!doctype html>
<html>
<head><meta charset="utf-8"><title>DeBros Auth</title>
<style>body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:2rem;max-width:720px}input,button,select{font-size:1rem;padding:.5rem;margin:.25rem 0}code{background:#f5f5f5;padding:.2rem .4rem;border-radius:4px}</style>
</head>
<body>
<h2>Authenticate with Wallet to Get API Key</h2>
<p>This will create or return an API key for namespace <code id="ns"></code> on gateway <code id="gw"></code>.</p>
<label>Wallet Address</label><br>
<input id="wallet" placeholder="0x..." style="width:100%%"/><br>
<label>Plan</label><br>
<select id="plan"><option value="free">free</option><option value="premium">premium (0.1 ETH)</option></select><br>
<button id="connect">Connect Wallet</button>
<button id="sign">Sign & Generate API Key</button>
<pre id="out" style="white-space:pre-wrap"></pre>
<script>
const GATEWAY = %q;
const DEFAULT_NS = %q;
const DEFAULT_WALLET = %q;
document.getElementById('gw').textContent = GATEWAY;
document.getElementById('ns').textContent = DEFAULT_NS;
document.getElementById('wallet').value = DEFAULT_WALLET;
document.getElementById('plan').value = %q;
const out = document.getElementById('out');
function log(m){ out.textContent += m + "\n" }
document.getElementById('connect').onclick = async () => {
if (!window.ethereum) { log('No wallet provider found (window.ethereum). Install MetaMask.'); return }
try { await window.ethereum.request({ method:'eth_requestAccounts' }); log('Wallet connected.'); } catch(e){ log('Connect failed: '+e.message) }
};
document.getElementById('sign').onclick = async () => {
try {
const wallet = document.getElementById('wallet').value.trim();
const plan = document.getElementById('plan').value;
if (!/^0x[0-9a-fA-F]{40}$/.test(wallet)) { log('Enter a valid EVM address'); return }
// Request nonce
const ch = await fetch(GATEWAY+"/v1/auth/challenge", {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({wallet, purpose:'api_key', namespace: DEFAULT_NS})});
if (!ch.ok) { const t = await ch.text(); log('Challenge failed: '+t); return }
const cj = await ch.json();
const nonce = cj.nonce;
// Sign nonce
let sig = await window.ethereum.request({ method:'personal_sign', params:[ nonce, wallet ] });
// Issue or fetch API key
const resp = await fetch(GATEWAY+"/v1/auth/api-key", {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({wallet, nonce, signature: sig, namespace: DEFAULT_NS, plan})});
if (!resp.ok) { const t = await resp.text(); log('Issue API key failed: '+t); return }
const data = await resp.json();
log('API Key: '+data.api_key+'\nNamespace: '+data.namespace);
// Send back to CLI
await fetch('/callback', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data)});
} catch(e){ log('Error: '+e.message) }
};
</script>
</body></html>`, gatewayURL, namespace, wallet, plan)
})
// Callback to deliver API key back to CLI
mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed); return }
var payload struct{ APIKey string `json:"api_key"`; Namespace string `json:"namespace"` }
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { w.WriteHeader(http.StatusBadRequest); return }
if strings.TrimSpace(payload.APIKey) == "" { w.WriteHeader(http.StatusBadRequest); return }
select { case resCh <- result{APIKey: payload.APIKey, Namespace: payload.Namespace}: default: }
_, _ = w.Write([]byte("ok"))
go func(){ time.Sleep(500*time.Millisecond); _ = srv.Close() }()
})
srv.Handler = mux
// Open browser
url := listenURL
go func(){
// Try to open in default browser
_ = openBrowser(url)
}()
// Serve and wait for result or timeout
go func(){ _ = srv.Serve(ln) }()
fmt.Printf("🌐 Please complete authentication in your browser: %s\n", url)
select {
case r := <-resCh:
fmt.Printf("✅ API Key issued for namespace '%s'\n", r.Namespace)
fmt.Printf("%s\n", r.APIKey)
case <-time.After(5 * time.Minute):
fmt.Fprintf(os.Stderr, "Timed out waiting for wallet signature.\n")
_ = srv.Close()
os.Exit(1)
}
}
func openBrowser(target string) error {
cmds := [][]string{
{"xdg-open", target},
{"open", target},
{"cmd", "/c", "start", target},
}
for _, c := range cmds {
cmd := exec.Command(c[0], c[1:]...)
if err := cmd.Start(); err == nil { return nil }
}
log.Printf("Please open %s manually", target)
return nil
}
// getenvDefault returns env var or default if empty/undefined.
func getenvDefault(key, def string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return def
}
func handleConnect(peerAddr string) {
client, err := createClient()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create client: %v\n", err)
os.Exit(1)
}
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err = client.Network().ConnectToPeer(ctx, peerAddr)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to connect to peer: %v\n", err)
os.Exit(1)
}
fmt.Printf("✅ Connected to peer: %s\n", peerAddr)
}
func handlePeerID() {
// Try to get peer ID from running network first
client, err := createClient()
if err == nil {
defer client.Disconnect()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
if status, err := client.Network().GetStatus(ctx); err == nil {
if format == "json" {
printJSON(map[string]string{"peer_id": status.NodeID})
} else {
fmt.Printf("🆔 Peer ID: %s\n", status.NodeID)
}
return
}
}
// Fallback: try to extract from local identity files
identityPaths := []string{
"/opt/debros/data/node/identity.key",
"/opt/debros/data/bootstrap/identity.key",
"/opt/debros/keys/node/identity.key",
"./data/node/identity.key",
"./data/bootstrap/identity.key",
}
for _, path := range identityPaths {
if peerID := extractPeerIDFromFile(path); peerID != "" {
if format == "json" {
printJSON(map[string]string{"peer_id": peerID, "source": "local_identity"})
} else {
fmt.Printf("🆔 Peer ID: %s\n", peerID)
fmt.Printf("📂 Source: %s\n", path)
}
return
}
}
// Check peer.info files as last resort
peerInfoPaths := []string{
"/opt/debros/data/node/peer.info",
"/opt/debros/data/bootstrap/peer.info",
"./data/node/peer.info",
"./data/bootstrap/peer.info",
}
for _, path := range peerInfoPaths {
if data, err := os.ReadFile(path); err == nil {
multiaddr := strings.TrimSpace(string(data))
if peerID := extractPeerIDFromMultiaddr(multiaddr); peerID != "" {
if format == "json" {
printJSON(map[string]string{"peer_id": peerID, "source": "peer_info"})
} else {
fmt.Printf("🆔 Peer ID: %s\n", peerID)
fmt.Printf("📂 Source: %s\n", path)
}
return
}
}
}
fmt.Fprintf(os.Stderr, "❌ Could not find peer ID. Make sure the node is running or identity files exist.\n")
os.Exit(1)
}
func createClient() (client.NetworkClient, error) {
config := client.DefaultClientConfig("network-cli")
networkClient, err := client.NewClient(config)
if err != nil {
return nil, err
}
if err := networkClient.Connect(); err != nil {
return nil, err
}
return networkClient, nil
}
// discoverBootstrapPeer tries to find the bootstrap peer from saved peer info
func discoverBootstrapPeer() string {
// Look for peer info in common locations
peerInfoPaths := []string{
"./data/bootstrap/peer.info",
"./data/test-bootstrap/peer.info",
"/tmp/bootstrap-peer.info",
}
for _, path := range peerInfoPaths {
if data, err := os.ReadFile(path); err == nil {
peerAddr := strings.TrimSpace(string(data))
if peerAddr != "" {
// Only print discovery message in table format
if format != "json" {
fmt.Printf("🔍 Discovered bootstrap peer: %s\n", peerAddr)
}
return peerAddr
}
}
}
return "" // Return empty string if no peer info found
}
// tryDecodeBase64 attempts to decode a string as base64, returns original if not valid base64
func tryDecodeBase64(s string) string {
// Only try to decode if it looks like base64 (no spaces, reasonable length)
if len(s) > 0 && len(s)%4 == 0 && !strings.ContainsAny(s, " \n\r\t") {
if decoded, err := base64.StdEncoding.DecodeString(s); err == nil {
// Check if decoded result looks like readable text
decodedStr := string(decoded)
if isPrintableText(decodedStr) {
return decodedStr
}
}
}
return s
}
// isPrintableText checks if a string contains mostly printable characters
func isPrintableText(s string) bool {
printableCount := 0
for _, r := range s {
if r >= 32 && r <= 126 || r == '\n' || r == '\r' || r == '\t' {
printableCount++
}
}
return len(s) > 0 && float64(printableCount)/float64(len(s)) > 0.8
}
func showHelp() {
fmt.Printf("Network CLI - Distributed P2P Network Management Tool\n\n")
fmt.Printf("Usage: network-cli <command> [args...]\n\n")
fmt.Printf("Commands:\n")
fmt.Printf(" health - Check network health\n")
fmt.Printf(" peers - List connected peers\n")
fmt.Printf(" status - Show network status\n")
fmt.Printf(" peer-id - Show this node's peer ID\n")
fmt.Printf(" query <sql> - Execute database query\n")
fmt.Printf(" storage get <key> - Get value from storage\n")
fmt.Printf(" storage put <key> <value> - Store value in storage\n")
fmt.Printf(" storage list [prefix] - List storage keys\n")
fmt.Printf(" pubsub publish <topic> <msg> - Publish message\n")
fmt.Printf(" pubsub subscribe <topic> [duration] - Subscribe to topic\n")
fmt.Printf(" pubsub topics - List topics\n")
fmt.Printf(" connect <peer_address> - Connect to peer\n")
fmt.Printf(" auth [--gateway URL] [--namespace NS] [--wallet 0x..] [--plan free|premium] - Obtain API key via wallet signature\n")
fmt.Printf(" help - Show this help\n\n")
fmt.Printf("Global Flags:\n")
fmt.Printf(" -b, --bootstrap <addr> - Bootstrap peer address (default: /ip4/127.0.0.1/tcp/4001)\n")
fmt.Printf(" -f, --format <format> - Output format: table, json (default: table)\n")
fmt.Printf(" -t, --timeout <duration> - Operation timeout (default: 30s)\n")
fmt.Printf(" --production - Connect to production bootstrap peers\n\n")
fmt.Printf("Examples:\n")
fmt.Printf(" network-cli health\n")
fmt.Printf(" network-cli peer-id\n")
fmt.Printf(" network-cli peer-id --format json\n")
fmt.Printf(" network-cli peers --format json\n")
fmt.Printf(" network-cli peers --production\n")
fmt.Printf(" network-cli storage put user:123 '{\"name\":\"Alice\"}'\n")
fmt.Printf(" network-cli pubsub subscribe notifications 1m\n")
}
// Print functions
func printHealth(health *client.HealthStatus) {
fmt.Printf("🏥 Network Health\n")
fmt.Printf("Status: %s\n", getStatusEmoji(health.Status)+health.Status)
fmt.Printf("Last Updated: %s\n", health.LastUpdated.Format("2006-01-02 15:04:05"))
fmt.Printf("Response Time: %v\n", health.ResponseTime)
fmt.Printf("\nChecks:\n")
for check, status := range health.Checks {
emoji := "✅"
if status != "ok" {
emoji = "❌"
}
fmt.Printf(" %s %s: %s\n", emoji, check, status)
}
}
func printPeers(peers []client.PeerInfo) {
fmt.Printf("👥 Connected Peers (%d)\n\n", len(peers))
if len(peers) == 0 {
fmt.Printf("No peers connected\n")
return
}
for i, peer := range peers {
connEmoji := "🔴"
if peer.Connected {
connEmoji = "🟢"
}
fmt.Printf("%d. %s %s\n", i+1, connEmoji, peer.ID)
fmt.Printf(" Addresses: %v\n", peer.Addresses)
fmt.Printf(" Last Seen: %s\n", peer.LastSeen.Format("2006-01-02 15:04:05"))
fmt.Println()
}
}
func printStatus(status *client.NetworkStatus) {
fmt.Printf("🌐 Network Status\n")
fmt.Printf("Node ID: %s\n", status.NodeID)
fmt.Printf("Connected: %s\n", getBoolEmoji(status.Connected)+strconv.FormatBool(status.Connected))
fmt.Printf("Peer Count: %d\n", status.PeerCount)
fmt.Printf("Database Size: %s\n", formatBytes(status.DatabaseSize))
fmt.Printf("Uptime: %v\n", status.Uptime.Round(time.Second))
}
func printQueryResult(result *client.QueryResult) {
fmt.Printf("📊 Query Result\n")
fmt.Printf("Rows: %d\n\n", result.Count)
if len(result.Rows) == 0 {
fmt.Printf("No data returned\n")
return
}
// Print header
for i, col := range result.Columns {
if i > 0 {
fmt.Printf(" | ")
}
fmt.Printf("%-15s", col)
}
fmt.Println()
// Print separator
for i := range result.Columns {
if i > 0 {
fmt.Printf("-+-")
}
fmt.Printf("%-15s", "---------------")
}
fmt.Println()
// Print rows
for _, row := range result.Rows {
for i, cell := range row {
if i > 0 {
fmt.Printf(" | ")
}
fmt.Printf("%-15v", cell)
}
fmt.Println()
}
}
func printJSON(data interface{}) {
jsonData, err := json.MarshalIndent(data, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to marshal JSON: %v\n", err)
return
}
fmt.Println(string(jsonData))
}
// Helper functions
func getStatusEmoji(status string) string {
switch status {
case "healthy":
return "🟢 "
case "degraded":
return "🟡 "
case "unhealthy":
return "🔴 "
default:
return "⚪ "
}
}
func getBoolEmoji(b bool) string {
if b {
return "✅ "
}
return "❌ "
}
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// extractPeerIDFromFile extracts peer ID from an identity key file
func extractPeerIDFromFile(keyFile string) string {
// Read the identity key file
data, err := os.ReadFile(keyFile)
if err != nil {
return ""
}
// Unmarshal the private key
priv, err := crypto.UnmarshalPrivateKey(data)
if err != nil {
return ""
}
// Get the public key
pub := priv.GetPublic()
// Get the peer ID
peerID, err := peer.IDFromPublicKey(pub)
if err != nil {
return ""
}
return peerID.String()
}
// extractPeerIDFromMultiaddr extracts the peer ID from a multiaddr string
func extractPeerIDFromMultiaddr(multiaddr string) string {
// Look for /p2p/ followed by the peer ID
parts := strings.Split(multiaddr, "/p2p/")
if len(parts) >= 2 {
return parts[1]
}
return ""
}