Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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
123 changes: 32 additions & 91 deletions cmd/puniversald/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,43 @@ package main

import (
"context"
"crypto/ed25519"
"encoding/hex"
"encoding/json"
"fmt"
"strings"

"github.com/libp2p/go-libp2p/core/crypto"
"github.com/libp2p/go-libp2p/core/peer"
"path/filepath"

sdkversion "github.com/cosmos/cosmos-sdk/version"
"github.com/pushchain/push-chain-node/universalClient/config"
"github.com/pushchain/push-chain-node/universalClient/constant"
cosmosevmcmd "github.com/cosmos/evm/client"
uvconfig "github.com/pushchain/push-chain-node/universalClient/config"
"github.com/pushchain/push-chain-node/universalClient/core"
"github.com/spf13/cobra"

cosmosevmcmd "github.com/cosmos/evm/client"
)

const flagHome = "home"

func InitRootCmd(rootCmd *cobra.Command) {
rootCmd.PersistentFlags().String(flagHome, uvconfig.DefaultNodeHome(), "node home directory")

rootCmd.AddCommand(versionCmd())
rootCmd.AddCommand(startCmd())
rootCmd.AddCommand(initCmd())
rootCmd.AddCommand(cosmosevmcmd.KeyCommands(constant.DefaultNodeHome, true))
rootCmd.AddCommand(tssPeerIDCmd())
rootCmd.AddCommand(cosmosevmcmd.KeyCommands(uvconfig.DefaultNodeHome(), true))
}

// getHome reads the --home flag, falling back to DefaultNodeHome.
func getHome(cmd *cobra.Command) string {
home, _ := cmd.Flags().GetString(flagHome)
if home == "" {
home = uvconfig.DefaultNodeHome()
}
return home
}

func versionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print universal validator version info",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Name: %s\n", sdkversion.Name)
fmt.Printf("App Name: %s\n", sdkversion.AppName)
fmt.Printf("Name: puniversald\n")
fmt.Printf("Version: %s\n", sdkversion.Version)
fmt.Printf("Commit: %s\n", sdkversion.Commit)
fmt.Printf("Build Tags: %s\n", sdkversion.BuildTags)
Expand All @@ -43,55 +47,50 @@ func versionCmd() *cobra.Command {
}

func initCmd() *cobra.Command {
cmd := &cobra.Command{
return &cobra.Command{
Use: "init",
Short: "Initialize configuration file",
Long: `Initialize the configuration file with default values.

This command creates a default configuration file at:
~/.puniversal/config/pushuv_config.json

You can edit this file to customize your universal validator settings.`,
By default creates the config at ~/.puniversal/config/pushuv_config.json.
Use --home to specify a different directory.`,
RunE: func(cmd *cobra.Command, args []string) error {
// Load default config
defaultCfg, err := config.LoadDefaultConfig()
home := getHome(cmd)

defaultCfg, err := uvconfig.LoadDefaultConfig()
if err != nil {
return fmt.Errorf("failed to load default config: %w", err)
}

// Save to config directory
if err := config.Save(&defaultCfg, constant.DefaultNodeHome); err != nil {
if err := uvconfig.Save(&defaultCfg, home); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}

configPath := fmt.Sprintf("%s/%s/%s", constant.DefaultNodeHome, constant.ConfigSubdir, constant.ConfigFileName)
fmt.Printf("✅ Configuration file initialized at: %s\n", configPath)
fmt.Println("You can now edit this file to customize your settings.")
configPath := filepath.Join(home, uvconfig.ConfigSubdir, uvconfig.ConfigFileName)
fmt.Printf("Configuration file initialized at: %s\n", configPath)
return nil
},
}
return cmd
}

func startCmd() *cobra.Command {
cmd := &cobra.Command{
return &cobra.Command{
Use: "start",
Short: "Start the universal message handler",
Short: "Start the universal validator",
RunE: func(cmd *cobra.Command, args []string) error {
// --- Step 1: Load config ---
loadedCfg, err := config.Load(constant.DefaultNodeHome)
home := getHome(cmd)

loadedCfg, err := uvconfig.Load(home)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}

// Print loaded config as JSON
configJSON, err := json.MarshalIndent(loadedCfg, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
fmt.Printf("\n=== Loaded Configuration ===\n%s\n===========================\n\n", string(configJSON))

// --- Step 2: Start client ---
ctx := context.Background()
client, err := core.NewUniversalClient(ctx, &loadedCfg)
if err != nil {
Expand All @@ -100,63 +99,5 @@ func startCmd() *cobra.Command {
return client.Start()
},
}
return cmd
}

// tssPeerIDCmd computes and prints the libp2p peer ID from a TSS private key hex string.
// This is used during devnet setup to derive the peer ID for universal validator registration.
func tssPeerIDCmd() *cobra.Command {
var privateKeyHex string

cmd := &cobra.Command{
Use: "tss-peer-id",
Short: "Compute libp2p peer ID from TSS private key hex",
Long: `Compute the libp2p peer ID from a 32-byte hex-encoded Ed25519 seed.

This is used during devnet setup to derive the peer ID that matches
what the TSS node will use, for universal validator registration.

Example:
puniversald tss-peer-id --private-key 0101010101010101010101010101010101010101010101010101010101010101`,
RunE: func(cmd *cobra.Command, args []string) error {
privateKeyHex = strings.TrimSpace(privateKeyHex)

// Decode hex to bytes
keyBytes, err := hex.DecodeString(privateKeyHex)
if err != nil {
return fmt.Errorf("invalid hex: %w", err)
}
if len(keyBytes) != 32 {
return fmt.Errorf("expected 32 bytes, got %d", len(keyBytes))
}

// Create Ed25519 key from seed
privKey := ed25519.NewKeyFromSeed(keyBytes)
pubKey := privKey.Public().(ed25519.PublicKey)

// Convert to libp2p format (64 bytes: 32 priv seed + 32 pub)
libp2pKeyBytes := make([]byte, 64)
copy(libp2pKeyBytes[:32], privKey[:32])
copy(libp2pKeyBytes[32:], pubKey)

libp2pPrivKey, err := crypto.UnmarshalEd25519PrivateKey(libp2pKeyBytes)
if err != nil {
return fmt.Errorf("failed to unmarshal Ed25519 key: %w", err)
}

// Get peer ID from public key
peerID, err := peer.IDFromPrivateKey(libp2pPrivKey)
if err != nil {
return fmt.Errorf("failed to derive peer ID: %w", err)
}

fmt.Println(peerID.String())
return nil
},
}

cmd.Flags().StringVar(&privateKeyHex, "private-key", "", "Hex-encoded 32-byte Ed25519 seed")
cmd.MarkFlagRequired("private-key")

return cmd
}
4 changes: 2 additions & 2 deletions cmd/puniversald/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
cosmosevmkeyring "github.com/cosmos/evm/crypto/keyring"
"github.com/pushchain/push-chain-node/app"
"github.com/pushchain/push-chain-node/app/params"
"github.com/pushchain/push-chain-node/universalClient/constant"
uvconfig "github.com/pushchain/push-chain-node/universalClient/config"
"github.com/spf13/cobra"
wasmkeeper "github.com/CosmWasm/wasmd/x/wasm/keeper"
"cosmossdk.io/log"
Expand All @@ -39,7 +39,7 @@ func NewRootCmd() *cobra.Command {
WithLegacyAmino(encodingConfig.Amino).
WithInput(os.Stdin).
WithAccountRetriever(authtypes.AccountRetriever{}).
WithHomeDir(constant.DefaultNodeHome).
WithHomeDir(uvconfig.DefaultNodeHome()).
WithBroadcastMode(flags.FlagBroadcastMode).
WithKeyringOptions(cosmosevmkeyring.Option()).
WithLedgerHasProtobuf(true).
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ require (
github.com/joho/godotenv v1.5.1
github.com/mr-tron/base58 v1.2.0
github.com/near/borsh-go v0.3.1
github.com/pkg/errors v0.9.1
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.22.0
github.com/rs/zerolog v1.34.0
github.com/spf13/cast v1.9.2
Expand Down
5 changes: 4 additions & 1 deletion universalClient/api/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import "net/http"

// handleHealth handles GET /health
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
if _, err := w.Write([]byte("OK")); err != nil {
s.logger.Error().Err(err).Msg("Failed to write health response")
}
}
1 change: 1 addition & 0 deletions universalClient/api/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ func TestHandleHealth(t *testing.T) {

assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "OK", w.Body.String())
assert.Equal(t, "text/plain", w.Header().Get("Content-Type"))
})
}
49 changes: 21 additions & 28 deletions universalClient/api/server.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"context"
"fmt"
"net"
"net/http"
Expand All @@ -11,8 +12,9 @@ import (

// Server provides HTTP endpoints
type Server struct {
logger zerolog.Logger
server *http.Server
logger zerolog.Logger
server *http.Server
listener net.Listener
}

// NewServer creates a new Server instance
Expand All @@ -37,24 +39,14 @@ func (s *Server) Start() error {
return fmt.Errorf("query server is nil")
}

// Channel to signal server startup result
startupChan := make(chan error, 1)
ln, err := net.Listen("tcp", s.server.Addr)
if err != nil {
return fmt.Errorf("failed to bind to address %s: %w", s.server.Addr, err)
}
s.listener = ln

// Start server in goroutine
go func() {
// Create a test listener to verify the port is available
ln, err := net.Listen("tcp", s.server.Addr)
if err != nil {
startupChan <- fmt.Errorf("failed to bind to address %s: %w", s.server.Addr, err)
return
}
ln.Close()

// Signal successful startup check
startupChan <- nil

// Now start the actual server
err = s.server.ListenAndServe()
err := s.server.Serve(ln)
switch err {
case nil:
s.logger.Info().Msg("Query server stopped normally")
Expand All @@ -65,22 +57,23 @@ func (s *Server) Start() error {
}
}()

// Wait for startup result with timeout
select {
case err := <-startupChan:
if err != nil {
return err
}
return nil
case <-time.After(5 * time.Second):
return fmt.Errorf("server startup timeout")
return nil
}

// Addr returns the listener address, useful when started on port 0
func (s *Server) Addr() string {
if s.listener != nil {
return s.listener.Addr().String()
}
return ""
}

// Stop gracefully shuts down the HTTP server
func (s *Server) Stop() error {
if s.server != nil {
return s.server.Close()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return s.server.Shutdown(ctx)
}
return nil
}
Loading
Loading