Skip to content

Commit 3998fa6

Browse files
committed
feat: add edit-validator command and validator description fields
Add new edit-validator command for modifying validator metadata. Extend MyValidatorInfo with Website, Details, SecurityContact, and Identity fields across types, fetcher, and service layers. Update register flow, start command, snapshot handling, and related tests.
1 parent aec73f9 commit 3998fa6

16 files changed

Lines changed: 656 additions & 120 deletions
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"time"
8+
9+
"github.com/pushchain/push-validator-cli/internal/validator"
10+
)
11+
12+
// handleEditValidator orchestrates editing a validator's profile fields:
13+
// - verify node is running and validator is registered
14+
// - auto-derive key name
15+
// - prompt for fields to update
16+
// - submit edit-validator transaction
17+
func handleEditValidator(d *Deps) error {
18+
if err := checkNodeRunning(d.Sup); err != nil {
19+
return err
20+
}
21+
22+
p := getPrinter()
23+
cfg := d.Cfg
24+
25+
// Step 1: Check validator status
26+
if flagOutput != "json" {
27+
fmt.Println()
28+
fmt.Print(p.Colors.Apply(p.Colors.Theme.Prompt, p.Colors.Emoji("🔍")+" Checking validator status..."))
29+
}
30+
31+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
32+
myVal, statusErr := d.Fetcher.GetMyValidator(ctx, cfg)
33+
cancel()
34+
35+
if statusErr != nil {
36+
if flagOutput == "json" {
37+
getPrinter().JSON(map[string]any{"ok": false, "error": "failed to check validator status"})
38+
} else {
39+
fmt.Println()
40+
fmt.Println(p.Colors.Error(p.Colors.Emoji("❌") + " Failed to check validator status"))
41+
fmt.Println()
42+
}
43+
return fmt.Errorf("failed to check validator status: %w", statusErr)
44+
}
45+
46+
if !myVal.IsValidator {
47+
if flagOutput == "json" {
48+
getPrinter().JSON(map[string]any{"ok": false, "error": "node is not registered as validator"})
49+
} else {
50+
fmt.Println()
51+
fmt.Println(p.Colors.Warning(p.Colors.Emoji("⚠️") + " This node is not registered as a validator"))
52+
fmt.Println()
53+
fmt.Println(p.Colors.Info("Register first using:"))
54+
fmt.Println(p.Colors.Apply(p.Colors.Theme.Command, " push-validator register-validator"))
55+
fmt.Println()
56+
}
57+
return fmt.Errorf("node is not registered as validator")
58+
}
59+
60+
if flagOutput != "json" {
61+
fmt.Println(" " + p.Colors.Success(p.Colors.Emoji("✓")))
62+
}
63+
64+
// Step 2: Auto-derive key name
65+
defaultKeyName := getenvDefault("KEY_NAME", "validator-key")
66+
keyName := defaultKeyName
67+
68+
if myVal.Address != "" {
69+
addrCtx, addrCancel := context.WithTimeout(context.Background(), 10*time.Second)
70+
accountAddr, convErr := convertValidatorToAccountAddress(addrCtx, myVal.Address, d.Runner)
71+
addrCancel()
72+
if convErr == nil {
73+
keyCtx, keyCancel := context.WithTimeout(context.Background(), 10*time.Second)
74+
foundKey, findErr := findKeyNameByAddress(keyCtx, cfg, accountAddr, d.Runner)
75+
keyCancel()
76+
if findErr == nil {
77+
keyName = foundKey
78+
if flagOutput != "json" {
79+
fmt.Printf("%s Using key: %s\n", p.Colors.Emoji("🔑"), keyName)
80+
}
81+
}
82+
}
83+
}
84+
85+
// Step 3: Prompt for fields
86+
if flagOutput != "json" {
87+
fmt.Println()
88+
p.Section("Edit Validator Profile")
89+
fmt.Println()
90+
if myVal.Moniker != "" {
91+
fmt.Printf(" Current moniker: %s\n", p.Colors.Apply(p.Colors.Theme.Value, myVal.Moniker))
92+
}
93+
if myVal.Website != "" {
94+
fmt.Printf(" Current website: %s\n", p.Colors.Apply(p.Colors.Theme.Value, myVal.Website))
95+
}
96+
if myVal.Details != "" {
97+
fmt.Printf(" Current details: %s\n", p.Colors.Apply(p.Colors.Theme.Value, myVal.Details))
98+
}
99+
if myVal.SecurityContact != "" {
100+
fmt.Printf(" Current security contact: %s\n", p.Colors.Apply(p.Colors.Theme.Value, myVal.SecurityContact))
101+
}
102+
if myVal.Identity != "" {
103+
fmt.Printf(" Current identity: %s\n", p.Colors.Apply(p.Colors.Theme.Value, myVal.Identity))
104+
}
105+
fmt.Println()
106+
}
107+
108+
prompter := d.Prompter
109+
var args validator.EditValidatorArgs
110+
args.KeyName = keyName
111+
112+
// Read from env vars first, then prompt interactively
113+
args.Moniker = os.Getenv("VALIDATOR_MONIKER")
114+
args.Website = os.Getenv("VALIDATOR_WEBSITE")
115+
args.Details = os.Getenv("VALIDATOR_DETAILS")
116+
args.Security = os.Getenv("VALIDATOR_SECURITY")
117+
args.Identity = os.Getenv("VALIDATOR_IDENTITY")
118+
119+
if prompter.IsInteractive() && flagOutput != "json" {
120+
monikerPrompt := "Enter new moniker (press ENTER to keep current): "
121+
if myVal.Moniker != "" {
122+
monikerPrompt = fmt.Sprintf("Enter new moniker (current: %s, press ENTER to keep): ", myVal.Moniker)
123+
}
124+
if moniker, err := prompter.ReadLine(monikerPrompt); err == nil && moniker != "" {
125+
args.Moniker = moniker
126+
}
127+
128+
websitePrompt := "Enter website URL (press ENTER to skip): "
129+
if myVal.Website != "" {
130+
websitePrompt = fmt.Sprintf("Enter website URL (current: %s, press ENTER to keep): ", myVal.Website)
131+
}
132+
if website, err := prompter.ReadLine(websitePrompt); err == nil && website != "" {
133+
args.Website = website
134+
}
135+
136+
detailsPrompt := "Enter description (press ENTER to skip): "
137+
if myVal.Details != "" {
138+
detailsPrompt = fmt.Sprintf("Enter description (current: %s, press ENTER to keep): ", myVal.Details)
139+
}
140+
if details, err := prompter.ReadLine(detailsPrompt); err == nil && details != "" {
141+
args.Details = details
142+
}
143+
144+
securityPrompt := "Enter security contact email (press ENTER to skip): "
145+
if myVal.SecurityContact != "" {
146+
securityPrompt = fmt.Sprintf("Enter security contact email (current: %s, press ENTER to keep): ", myVal.SecurityContact)
147+
}
148+
if security, err := prompter.ReadLine(securityPrompt); err == nil && security != "" {
149+
args.Security = security
150+
}
151+
152+
identityPrompt := "Enter Keybase identity for logo (16-digit ID, press ENTER to skip): "
153+
if myVal.Identity != "" {
154+
identityPrompt = fmt.Sprintf("Enter Keybase identity for logo (current: %s, press ENTER to keep): ", myVal.Identity)
155+
}
156+
if identity, err := prompter.ReadLine(identityPrompt); err == nil && identity != "" {
157+
args.Identity = identity
158+
}
159+
}
160+
161+
// Check if anything was provided
162+
if args.Moniker == "" && args.Website == "" && args.Details == "" && args.Security == "" && args.Identity == "" {
163+
if flagOutput == "json" {
164+
getPrinter().JSON(map[string]any{"ok": true, "message": "no changes to make"})
165+
} else {
166+
fmt.Println(p.Colors.Info("No changes to make."))
167+
fmt.Println()
168+
}
169+
return nil
170+
}
171+
172+
// Step 4: Submit transaction
173+
if flagOutput != "json" {
174+
fmt.Println()
175+
fmt.Print(p.Colors.Apply(p.Colors.Theme.Prompt, p.Colors.Emoji("📤")+" Submitting edit-validator transaction..."))
176+
}
177+
178+
txCtx, txCancel := context.WithTimeout(context.Background(), 90*time.Second)
179+
defer txCancel()
180+
181+
txHash, err := d.Validator.EditValidator(txCtx, args)
182+
if err != nil {
183+
if flagOutput == "json" {
184+
getPrinter().JSON(map[string]any{"ok": false, "error": err.Error()})
185+
} else {
186+
fmt.Println()
187+
fmt.Println(p.Colors.Error(p.Colors.Emoji("❌") + " Edit validator failed"))
188+
fmt.Println()
189+
fmt.Printf("Error: %v\n", err)
190+
fmt.Println()
191+
}
192+
return fmt.Errorf("edit validator failed: %w", err)
193+
}
194+
195+
if flagOutput != "json" {
196+
fmt.Println(" " + p.Colors.Success(p.Colors.Emoji("✓")))
197+
}
198+
199+
// Success output
200+
if flagOutput == "json" {
201+
getPrinter().JSON(map[string]any{"ok": true, "txhash": txHash})
202+
} else {
203+
fmt.Println()
204+
p.Success(p.Colors.Emoji("✅") + " Validator profile updated successfully!")
205+
fmt.Println()
206+
p.KeyValueLine("Transaction Hash", txHash, "green")
207+
if args.Moniker != "" {
208+
p.KeyValueLine("New Moniker", args.Moniker, "blue")
209+
}
210+
if args.Website != "" {
211+
p.KeyValueLine("Website", args.Website, "blue")
212+
}
213+
if args.Details != "" {
214+
p.KeyValueLine("Details", args.Details, "dim")
215+
}
216+
if args.Security != "" {
217+
p.KeyValueLine("Security Contact", args.Security, "dim")
218+
}
219+
if args.Identity != "" {
220+
p.KeyValueLine("Identity", args.Identity, "dim")
221+
}
222+
fmt.Println()
223+
}
224+
return nil
225+
}

0 commit comments

Comments
 (0)