Skip to content

Commit 2e72553

Browse files
committed
fixes
1 parent 73d8b13 commit 2e72553

33 files changed

Lines changed: 1673 additions & 1424 deletions

cmd/push-validator/cmd_balance.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ func handleBalance(cfg config.Config, args []string) error {
3535

3636
// Convert hex address (0x...) to bech32 if needed
3737
if strings.HasPrefix(addr, "0x") || strings.HasPrefix(addr, "0X") {
38-
bech32Addr, convErr := hexToBech32Address(addr)
38+
convCtx, convCancel := context.WithTimeout(context.Background(), 10*time.Second)
39+
bech32Addr, convErr := hexToBech32Address(convCtx, addr)
40+
convCancel()
3941
if convErr != nil {
4042
if flagOutput == "json" { getPrinter().JSON(map[string]any{"ok": false, "error": convErr.Error(), "address": addr}) } else { getPrinter().Error(fmt.Sprintf("address conversion error: %v", convErr)) }
4143
return convErr

cmd/push-validator/cmd_doctor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func checkP2PPeers(cfg config.Config, c *ui.ColorConfig) checkResult {
216216
func checkRemoteConnectivity(cfg config.Config, c *ui.ColorConfig) checkResult {
217217
result := checkResult{Name: "Remote Connectivity"}
218218

219-
remote := "https://" + strings.TrimSuffix(cfg.GenesisDomain, "/") + ":443"
219+
remote := cfg.RemoteRPCURL()
220220

221221
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
222222
defer cancel()

cmd/push-validator/cmd_increase_stake.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ func handleIncreaseStake(cfg config.Config) {
6767
p.KeyValueLine("Address", myValInfo.Address, "dim")
6868

6969
// Get and display EVM address
70-
evmAddr, evmErr := getEVMAddress(myValInfo.Address)
70+
evmCtx, evmCancel := context.WithTimeout(context.Background(), 10*time.Second)
71+
evmAddr, evmErr := getEVMAddress(evmCtx, myValInfo.Address)
72+
evmCancel()
7173
if evmErr == nil {
7274
p.KeyValueLine("EVM Address", evmAddr, "dim")
7375
}
@@ -78,7 +80,9 @@ func handleIncreaseStake(cfg config.Config) {
7880
fmt.Println()
7981

8082
// Convert validator operator address to account address
81-
accountAddr, convErr := convertValidatorToAccountAddress(myValInfo.Address)
83+
convCtx, convCancel := context.WithTimeout(context.Background(), 10*time.Second)
84+
accountAddr, convErr := convertValidatorToAccountAddress(convCtx, myValInfo.Address)
85+
convCancel()
8286
if convErr != nil {
8387
if flagOutput == "json" {
8488
getPrinter().JSON(map[string]any{"ok": false, "error": convErr.Error()})
@@ -195,10 +199,15 @@ func handleIncreaseStake(cfg config.Config) {
195199
if myValInfo.Address != "" {
196200
// We already have accountAddr from the balance check above, but need to recalculate
197201
// in case that logic changes in the future
198-
accountAddr, convErr := convertValidatorToAccountAddress(myValInfo.Address)
202+
addrCtx, addrCancel := context.WithTimeout(context.Background(), 10*time.Second)
203+
accountAddr, convErr := convertValidatorToAccountAddress(addrCtx, myValInfo.Address)
204+
addrCancel()
199205
if convErr == nil {
200206
// Try to find the key in the keyring
201-
if foundKey, findErr := findKeyNameByAddress(cfg, accountAddr); findErr == nil {
207+
keyCtx, keyCancel := context.WithTimeout(context.Background(), 10*time.Second)
208+
foundKey, findErr := findKeyNameByAddress(keyCtx, cfg, accountAddr)
209+
keyCancel()
210+
if findErr == nil {
202211
keyName = foundKey
203212
if flagOutput != "json" {
204213
fmt.Println()

cmd/push-validator/cmd_init.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/spf13/cobra"
7+
8+
"github.com/pushchain/push-validator-cli/internal/bootstrap"
9+
ui "github.com/pushchain/push-validator-cli/internal/ui"
10+
)
11+
12+
var (
13+
initMoniker string
14+
initChainID string
15+
initSnapshotURL string
16+
initSkipSnapshot bool
17+
)
18+
19+
var initNodeCmd = &cobra.Command{
20+
Use: "init",
21+
Short: "Initialize local node home",
22+
Hidden: true,
23+
RunE: func(cmd *cobra.Command, args []string) error {
24+
cfg := loadCfg()
25+
p := getPrinter()
26+
if initMoniker == "" {
27+
initMoniker = getenvDefault("MONIKER", "push-validator")
28+
}
29+
if initChainID == "" {
30+
initChainID = cfg.ChainID
31+
}
32+
if initSnapshotURL == "" {
33+
initSnapshotURL = cfg.SnapshotURL
34+
}
35+
36+
// Create progress callback that shows init steps
37+
progressCallback := func(msg string) {
38+
if flagOutput != "json" {
39+
fmt.Printf(" → %s\n", msg)
40+
}
41+
}
42+
43+
svc := bootstrap.New()
44+
if err := svc.Init(cmd.Context(), bootstrap.Options{
45+
HomeDir: cfg.HomeDir,
46+
ChainID: initChainID,
47+
Moniker: initMoniker,
48+
GenesisDomain: cfg.GenesisDomain,
49+
BinPath: findPchaind(),
50+
SnapshotURL: initSnapshotURL,
51+
Progress: progressCallback,
52+
SnapshotProgress: createSnapshotProgressCallback(flagOutput),
53+
SkipSnapshot: initSkipSnapshot,
54+
}); err != nil {
55+
ui.PrintError(ui.ErrorMessage{
56+
Problem: "Initialization failed",
57+
Causes: []string{
58+
"Network issue fetching genesis or status",
59+
"Incorrect --genesis-domain or RPC unreachable",
60+
"pchaind binary missing or not executable",
61+
},
62+
Actions: []string{
63+
"Verify connectivity: curl https://<genesis-domain>/status",
64+
"Set --genesis-domain to a working RPC host",
65+
"Ensure pchaind is installed and in PATH or pass --bin",
66+
},
67+
Hints: []string{"push-validator validators --output json"},
68+
})
69+
return err
70+
}
71+
// Only show success message when NOT in scripted mode (--skip-snapshot)
72+
// install.sh calls with --skip-snapshot and handles its own "Node initialized" message
73+
if flagOutput != "json" && !initSkipSnapshot {
74+
p.Success("Initialization complete")
75+
}
76+
return nil
77+
},
78+
}
79+
80+
func init() {
81+
initNodeCmd.Flags().StringVar(&initMoniker, "moniker", "", "Validator moniker")
82+
initNodeCmd.Flags().StringVar(&initChainID, "chain-id", "", "Chain ID")
83+
initNodeCmd.Flags().StringVar(&initSnapshotURL, "snapshot-url", "", "Snapshot download base URL")
84+
initNodeCmd.Flags().BoolVar(&initSkipSnapshot, "skip-snapshot", false, "Skip snapshot download (for separate step)")
85+
rootCmd.AddCommand(initNodeCmd)
86+
}

cmd/push-validator/cmd_logs.go

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -45,45 +45,38 @@ func handleLogs(sup process.Supervisor) error {
4545
}
4646
}
4747
}
48-
if interactive {
49-
var (
50-
origIn = os.Stdin
51-
origOut = os.Stdout
52-
)
53-
if tty != nil {
54-
os.Stdin = tty
55-
os.Stdout = tty
56-
}
48+
// Setup context for signal handling
49+
ctx, cancel := context.WithCancel(context.Background())
50+
defer cancel()
51+
52+
// Handle Ctrl+C
53+
sigs := make(chan os.Signal, 1)
54+
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
55+
go func() {
56+
<-sigs
57+
cancel()
58+
}()
59+
60+
var (
61+
origIn = os.Stdin
62+
origOut = os.Stdout
63+
)
64+
if tty != nil {
65+
os.Stdin = tty
66+
os.Stdout = tty
5767
defer func() {
58-
if tty != nil {
59-
_ = tty.Close()
60-
}
68+
_ = tty.Close()
6169
os.Stdin = origIn
6270
os.Stdout = origOut
6371
}()
64-
// Pass context.Background() - RunLogUIV2 handles Ctrl+C via raw terminal input
65-
return ui.RunLogUIV2(context.Background(), ui.LogUIOptions{
66-
LogPath: lp,
67-
BgKey: 'b',
68-
ShowFooter: true,
69-
NoColor: flagNoColor,
70-
})
71-
}
72-
if tty != nil {
73-
_ = tty.Close()
7472
}
7573

76-
getPrinter().Info(fmt.Sprintf("Tailing %s (Ctrl+C to stop)", lp))
77-
stop := make(chan struct{})
78-
sigs := make(chan os.Signal, 1)
79-
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
80-
go func() { <-sigs; close(stop) }()
81-
if err := process.TailFollow(lp, os.Stdout, stop); err != nil {
82-
fmt.Printf("tail error: %v\n", err)
83-
return err
84-
}
85-
// Print exit message after Ctrl+C
86-
fmt.Println()
87-
getPrinter().Success("Stopped tailing logs")
88-
return nil
74+
// RunLogUIV2 handles both interactive (with TUI) and non-interactive (tail -F) modes
75+
// It automatically detects TTY and falls back to simple tail when needed
76+
return ui.RunLogUIV2(ctx, ui.LogUIOptions{
77+
LogPath: lp,
78+
BgKey: 'b',
79+
ShowFooter: interactive,
80+
NoColor: flagNoColor,
81+
})
8982
}

cmd/push-validator/cmd_register.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,23 @@ import (
1818
"golang.org/x/term"
1919
)
2020

21+
const (
22+
// registrationRequiredBalance is the minimum balance needed to register (1.6 PC in wei)
23+
registrationRequiredBalance = "1600000000000000000"
24+
25+
// registrationMinStake is the minimum self-delegation amount (1.5 PC in wei)
26+
registrationMinStake = "1500000000000000000"
27+
28+
// registrationFeeReserve is the amount reserved for transaction fees (0.1 PC in wei)
29+
registrationFeeReserve = "100000000000000000"
30+
31+
// defaultCommissionRate is the default validator commission rate (10%)
32+
defaultCommissionRate = "0.10"
33+
34+
// defaultMinSelfDelegation is the default minimum self-delegation value
35+
defaultMinSelfDelegation = "1"
36+
)
37+
2138
var flagRegisterCheckOnly bool
2239

2340
// handleRegisterValidator is a compatibility wrapper that pulls
@@ -27,7 +44,7 @@ func handleRegisterValidator(cfg config.Config) {
2744
// Get defaults from env or use hardcoded fallbacks
2845
defaultMoniker := getenvDefault("MONIKER", "push-validator")
2946
defaultKeyName := getenvDefault("KEY_NAME", "validator-key")
30-
defaultAmount := getenvDefault("STAKE_AMOUNT", "1500000000000000000")
47+
defaultAmount := getenvDefault("STAKE_AMOUNT", registrationMinStake)
3148

3249
moniker := defaultMoniker
3350
keyName := defaultKeyName
@@ -195,29 +212,29 @@ func handleRegisterValidator(cfg config.Config) {
195212
input = strings.TrimSpace(input)
196213

197214
if input == "" {
198-
commissionRate = "0.10" // Default 10%
215+
commissionRate = defaultCommissionRate // Default 10%
199216
} else {
200217
// Parse and validate
201218
rate, err := strconv.ParseFloat(input, 64)
202219
if err != nil || rate < 1 || rate > 100 {
203220
fmt.Println(p.Colors.Error("⚠ Invalid commission rate. Using default 10%"))
204-
commissionRate = "0.10"
221+
commissionRate = defaultCommissionRate
205222
} else {
206223
// Convert percentage to decimal (e.g., 15 -> 0.15)
207224
commissionRate = fmt.Sprintf("%.2f", rate/100)
208225
}
209226
}
210227
fmt.Println()
211228
} else {
212-
commissionRate = getenvDefault("COMMISSION_RATE", "0.10")
229+
commissionRate = getenvDefault("COMMISSION_RATE", defaultCommissionRate)
213230
}
214231

215232
// Interactive mode - let user choose stake amount
216233
// Pass empty string to trigger the interactive stake selection prompt
217234
runRegisterValidator(cfg, moniker, keyName, "", commissionRate, importMnemonic)
218235
} else {
219236
// JSON mode or env vars set - use default/env amount
220-
commissionRate := getenvDefault("COMMISSION_RATE", "0.10")
237+
commissionRate := getenvDefault("COMMISSION_RATE", defaultCommissionRate)
221238
runRegisterValidator(cfg, moniker, keyName, defaultAmount, commissionRate, "")
222239
}
223240
}
@@ -313,7 +330,7 @@ func runRegisterValidator(cfg config.Config, moniker, keyName, amount, commissio
313330
if local == "" {
314331
local = "http://127.0.0.1:26657"
315332
}
316-
remoteHTTP := "https://" + strings.TrimSuffix(cfg.GenesisDomain, "/") + ":443"
333+
remoteHTTP := cfg.RemoteRPCURL()
317334
cliLocal := node.New(local)
318335
cliRemote := node.New(remoteHTTP)
319336
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
@@ -405,9 +422,6 @@ func runRegisterValidator(cfg config.Config, moniker, keyName, amount, commissio
405422
p.KeyValueLine("Cosmos Address", keyInfo.Address, "dim")
406423
fmt.Println()
407424
}
408-
const requiredBalance = "1600000000000000000"
409-
const minStake = "1500000000000000000" // 1.5 PC in wei
410-
const feeReserve = "100000000000000000" // 0.1 PC in wei for gas fees
411425
maxRetries := 10
412426
var finalBalance string
413427

@@ -424,7 +438,7 @@ func runRegisterValidator(cfg config.Config, moniker, keyName, amount, commissio
424438
balInt := new(big.Int)
425439
balInt.SetString(bal, 10)
426440
reqInt := new(big.Int)
427-
reqInt.SetString(requiredBalance, 10)
441+
reqInt.SetString(registrationRequiredBalance, 10)
428442
if balInt.Cmp(reqInt) >= 0 {
429443
fmt.Println(p.Colors.Success("✅ Sufficient balance"))
430444
finalBalance = bal
@@ -462,11 +476,11 @@ func runRegisterValidator(cfg config.Config, moniker, keyName, amount, commissio
462476
balInt := new(big.Int)
463477
balInt.SetString(finalBalance, 10)
464478
feeInt := new(big.Int)
465-
feeInt.SetString(feeReserve, 10)
479+
feeInt.SetString(registrationFeeReserve, 10)
466480
maxStakeable := new(big.Int).Sub(balInt, feeInt)
467481

468482
minStakeInt := new(big.Int)
469-
minStakeInt.SetString(minStake, 10)
483+
minStakeInt.SetString(registrationMinStake, 10)
470484

471485
// Display balance and staking range
472486
fmt.Println()
@@ -526,12 +540,12 @@ func runRegisterValidator(cfg config.Config, moniker, keyName, amount, commissio
526540
break
527541
}
528542
} else if stake == "" {
529-
stake = minStake
543+
stake = registrationMinStake
530544
}
531545
// Create fresh context for registration transaction (independent of earlier operations)
532546
regCtx, regCancel := context.WithTimeout(context.Background(), 90*time.Second)
533547
defer regCancel()
534-
txHash, err := v.Register(regCtx, validator.RegisterArgs{Moniker: moniker, Amount: stake, KeyName: keyName, CommissionRate: commissionRate, MinSelfDelegation: "1"})
548+
txHash, err := v.Register(regCtx, validator.RegisterArgs{Moniker: moniker, Amount: stake, KeyName: keyName, CommissionRate: commissionRate, MinSelfDelegation: defaultMinSelfDelegation})
535549
if err != nil {
536550
errMsg := err.Error()
537551
if flagOutput == "json" {

cmd/push-validator/cmd_reset.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ import (
1818
// handleReset stops the node (best-effort), clears chain data while
1919
// preserving the address book, and restarts the node. It emits JSON or text depending on --output.
2020
func handleReset(cfg config.Config, sup process.Supervisor) error {
21+
p := getPrinter()
22+
23+
// Require confirmation for destructive operation
24+
if flagOutput != "json" && !flagYes {
25+
if flagNonInteractive {
26+
return fmt.Errorf("reset requires confirmation: use --yes to confirm in non-interactive mode")
27+
}
28+
fmt.Println(p.Colors.Warning("⚠️ This will reset all chain data (address book will be kept)"))
29+
fmt.Println()
30+
fmt.Print(p.Colors.Apply(p.Colors.Theme.Prompt, "Confirm reset? (y/N): "))
31+
reader := bufio.NewReader(os.Stdin)
32+
response, _ := reader.ReadString('\n')
33+
if strings.ToLower(strings.TrimSpace(response)) != "y" {
34+
fmt.Println(p.Colors.Info("Reset cancelled"))
35+
return nil
36+
}
37+
}
38+
2139
wasRunning := sup.IsRunning()
2240

2341
// Stop node first and verify it stopped
@@ -141,6 +159,9 @@ func handleFullReset(cfg config.Config, sup process.Supervisor) error {
141159

142160
// Require explicit confirmation
143161
if !flagYes {
162+
if flagNonInteractive {
163+
return fmt.Errorf("full-reset requires confirmation: use --yes to confirm in non-interactive mode")
164+
}
144165
fmt.Print(p.Colors.Apply(p.Colors.Theme.Prompt, "Type 'yes' to confirm full reset: "))
145166
reader := bufio.NewReader(os.Stdin)
146167
response, _ := reader.ReadString('\n')

0 commit comments

Comments
 (0)