Skip to content

Commit bd82c8f

Browse files
committed
fixes
1 parent 905ddeb commit bd82c8f

File tree

3 files changed

+189
-17
lines changed

3 files changed

+189
-17
lines changed

cmd/push-validator/cmd_register.go

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func handleRegisterValidator(cfg config.Config) {
3131

3232
moniker := defaultMoniker
3333
keyName := defaultKeyName
34+
var importMnemonic string // Will hold mnemonic if user chooses to import
3435

3536
v := validator.NewWith(validator.Options{
3637
BinPath: findPchaind(),
@@ -168,12 +169,19 @@ func handleRegisterValidator(cfg config.Config) {
168169
newName = strings.TrimSpace(newName)
169170
if newName != "" {
170171
keyName = newName
172+
// Check if new key name also exists, if not, show wallet choice
173+
if !keyExists(cfg, keyName) {
174+
importMnemonic = promptWalletChoice(reader)
175+
}
171176
} else {
172177
// User chose to reuse existing key
173178
fmt.Println()
174179
fmt.Println(p.Colors.Success("✓ Proceeding with existing key"))
175180
fmt.Println()
176181
}
182+
} else {
183+
// Key doesn't exist - prompt for wallet creation method
184+
importMnemonic = promptWalletChoice(reader)
177185
}
178186
fmt.Println()
179187
}
@@ -206,11 +214,11 @@ func handleRegisterValidator(cfg config.Config) {
206214

207215
// Interactive mode - let user choose stake amount
208216
// Pass empty string to trigger the interactive stake selection prompt
209-
runRegisterValidator(cfg, moniker, keyName, "", commissionRate)
217+
runRegisterValidator(cfg, moniker, keyName, "", commissionRate, importMnemonic)
210218
} else {
211219
// JSON mode or env vars set - use default/env amount
212220
commissionRate := getenvDefault("COMMISSION_RATE", "0.10")
213-
runRegisterValidator(cfg, moniker, keyName, defaultAmount, commissionRate)
221+
runRegisterValidator(cfg, moniker, keyName, defaultAmount, commissionRate, "")
214222
}
215223
}
216224

@@ -224,13 +232,68 @@ func keyExists(cfg config.Config, keyName string) bool {
224232
return err == nil
225233
}
226234

235+
// promptWalletChoice prompts the user to choose between creating a new wallet or importing an existing one.
236+
// Returns the mnemonic if user chooses to import, empty string otherwise.
237+
func promptWalletChoice(reader *bufio.Reader) string {
238+
p := ui.NewPrinter(flagOutput)
239+
240+
fmt.Println()
241+
fmt.Println(p.Colors.Info("Wallet Setup"))
242+
fmt.Println(p.Colors.Separator(40))
243+
fmt.Println()
244+
fmt.Println(" [1] Create new wallet (generates new recovery phrase)")
245+
fmt.Println(" [2] Import existing wallet (use your recovery phrase)")
246+
fmt.Println()
247+
fmt.Print("Choose option [1]: ")
248+
249+
choice, _ := reader.ReadString('\n')
250+
choice = strings.TrimSpace(choice)
251+
252+
if choice != "2" {
253+
// Default to creating new wallet
254+
return ""
255+
}
256+
257+
// User chose to import
258+
fmt.Println()
259+
fmt.Println(p.Colors.Info("Enter your recovery mnemonic phrase (12 or 24 words):"))
260+
fmt.Println(p.Colors.Apply(p.Colors.Theme.Description, "(Words should be separated by spaces)"))
261+
fmt.Println()
262+
fmt.Print("> ")
263+
264+
mnemonic, err := reader.ReadString('\n')
265+
if err != nil {
266+
fmt.Println(p.Colors.Error(fmt.Sprintf("Error reading input: %v", err)))
267+
return ""
268+
}
269+
270+
// Normalize the mnemonic
271+
mnemonic = strings.TrimSpace(mnemonic)
272+
mnemonic = strings.Join(strings.Fields(mnemonic), " ") // Normalize whitespace
273+
mnemonic = strings.ToLower(mnemonic) // Convert to lowercase
274+
275+
// Validate mnemonic format
276+
if err := validator.ValidateMnemonic(mnemonic); err != nil {
277+
fmt.Println()
278+
fmt.Println(p.Colors.Error(fmt.Sprintf("Invalid mnemonic: %v", err)))
279+
fmt.Println()
280+
return ""
281+
}
282+
283+
fmt.Println()
284+
fmt.Println(p.Colors.Success("✓ Mnemonic format validated"))
285+
286+
return mnemonic
287+
}
288+
227289
// runRegisterValidator performs the end-to-end registration flow:
228290
// - verify node is not catching up
229-
// - ensure key exists
291+
// - ensure key exists (or import from mnemonic)
230292
// - wait for funding if necessary
231293
// - submit create-validator transaction
232294
// It prints text or JSON depending on --output.
233-
func runRegisterValidator(cfg config.Config, moniker, keyName, amount, commissionRate string) {
295+
// If importMnemonic is non-empty, the key will be imported from that mnemonic instead of creating a new one.
296+
func runRegisterValidator(cfg config.Config, moniker, keyName, amount, commissionRate, importMnemonic string) {
234297
savedStdin := os.Stdin
235298
var tty *os.File
236299
if !flagNonInteractive && !term.IsTerminal(int(savedStdin.Fd())) {
@@ -270,14 +333,40 @@ func runRegisterValidator(cfg config.Config, moniker, keyName, amount, commissio
270333
v := validator.NewWith(validator.Options{BinPath: findPchaind(), HomeDir: cfg.HomeDir, ChainID: cfg.ChainID, Keyring: cfg.KeyringBackend, GenesisDomain: cfg.GenesisDomain, Denom: cfg.Denom})
271334
ctx2, cancel2 := context.WithTimeout(context.Background(), 20*time.Second)
272335
defer cancel2()
273-
keyInfo, err := v.EnsureKey(ctx2, keyName)
274-
if err != nil {
275-
if flagOutput == "json" {
276-
getPrinter().JSON(map[string]any{"ok": false, "error": err.Error()})
277-
} else {
278-
fmt.Printf("key error: %v\n", err)
336+
337+
// Handle key creation or import based on importMnemonic
338+
var keyInfo validator.KeyInfo
339+
var err error
340+
var wasImported bool
341+
342+
if importMnemonic != "" {
343+
// Import key from mnemonic
344+
keyInfo, err = v.ImportKey(ctx2, keyName, importMnemonic)
345+
wasImported = true
346+
if err != nil {
347+
if flagOutput == "json" {
348+
getPrinter().JSON(map[string]any{"ok": false, "error": err.Error()})
349+
} else {
350+
p := ui.NewPrinter(flagOutput)
351+
fmt.Println()
352+
fmt.Println(p.Colors.Error("Failed to import wallet"))
353+
fmt.Printf("Error: %v\n\n", err)
354+
fmt.Println(p.Colors.Info("Please verify your mnemonic phrase and try again."))
355+
fmt.Println()
356+
}
357+
return
358+
}
359+
} else {
360+
// Create new key or use existing (original behavior)
361+
keyInfo, err = v.EnsureKey(ctx2, keyName)
362+
if err != nil {
363+
if flagOutput == "json" {
364+
getPrinter().JSON(map[string]any{"ok": false, "error": err.Error()})
365+
} else {
366+
fmt.Printf("key error: %v\n", err)
367+
}
368+
return
279369
}
280-
return
281370
}
282371

283372
evmAddr, err := v.GetEVMAddress(ctx2, keyInfo.Address)
@@ -288,16 +377,21 @@ func runRegisterValidator(cfg config.Config, moniker, keyName, amount, commissio
288377
p := ui.NewPrinter(flagOutput)
289378

290379
if flagOutput != "json" {
291-
// Display mnemonic if this is a new key
380+
// Display appropriate message based on key creation method
292381
if keyInfo.Mnemonic != "" {
293-
// Display mnemonic in prominent box
382+
// New key was created - display mnemonic in prominent box
294383
p.MnemonicBox(keyInfo.Mnemonic)
295384
fmt.Println()
296385

297386
// Warning message in yellow
298387
fmt.Println(p.Colors.Warning("**Important** Write this mnemonic phrase in a safe place."))
299388
fmt.Println(p.Colors.Warning("It is the only way to recover your account if you ever forget your password."))
300389
fmt.Println()
390+
} else if wasImported {
391+
// Key was imported from mnemonic - show success message
392+
fmt.Println(p.Colors.Success(fmt.Sprintf("✓ Wallet imported successfully: %s", keyInfo.Name)))
393+
fmt.Println(p.Colors.Apply(p.Colors.Theme.Description, " (Keep your recovery phrase safe - it controls this wallet)"))
394+
fmt.Println()
301395
} else {
302396
// Existing key - show clear status with reminder
303397
fmt.Println(p.Colors.Success(fmt.Sprintf("✓ Using existing key: %s", keyInfo.Name)))

internal/validator/service.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ type KeyInfo struct {
1313

1414
// Service handles key ops, balances, validator detection, and registration flow.
1515
type Service interface {
16-
EnsureKey(ctx context.Context, name string) (KeyInfo, error) // returns key info
17-
GetEVMAddress(ctx context.Context, addr string) (string, error) // returns hex/EVM address
16+
EnsureKey(ctx context.Context, name string) (KeyInfo, error) // returns key info
17+
ImportKey(ctx context.Context, name string, mnemonic string) (KeyInfo, error) // imports key from mnemonic
18+
GetEVMAddress(ctx context.Context, addr string) (string, error) // returns hex/EVM address
1819
IsValidator(ctx context.Context, addr string) (bool, error)
1920
Balance(ctx context.Context, addr string) (string, error) // denom string for now
2021
Register(ctx context.Context, args RegisterArgs) (string, error) // returns tx hash
@@ -42,8 +43,9 @@ func New() Service { return &noop{} }
4243

4344
type noop struct{}
4445

45-
func (n *noop) EnsureKey(ctx context.Context, name string) (KeyInfo, error) { return KeyInfo{}, nil }
46-
func (n *noop) GetEVMAddress(ctx context.Context, addr string) (string, error) { return "", nil }
46+
func (n *noop) EnsureKey(ctx context.Context, name string) (KeyInfo, error) { return KeyInfo{}, nil }
47+
func (n *noop) ImportKey(ctx context.Context, name string, mnemonic string) (KeyInfo, error) { return KeyInfo{}, nil }
48+
func (n *noop) GetEVMAddress(ctx context.Context, addr string) (string, error) { return "", nil }
4749
func (n *noop) IsValidator(ctx context.Context, addr string) (bool, error) { return false, nil }
4850
func (n *noop) Balance(ctx context.Context, addr string) (string, error) { return "0", nil }
4951
func (n *noop) Register(ctx context.Context, args RegisterArgs) (string, error) { return "", nil }

internal/validator/service_impl.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,82 @@ func extractMnemonic(output string) string {
136136
return ""
137137
}
138138

139+
// ValidateMnemonic performs basic validation of a mnemonic phrase.
140+
// Returns nil if valid, error with details if invalid.
141+
func ValidateMnemonic(mnemonic string) error {
142+
words := strings.Fields(mnemonic)
143+
wordCount := len(words)
144+
145+
// BIP39 supports 12, 15, 18, 21, or 24 word mnemonics
146+
validCounts := map[int]bool{12: true, 15: true, 18: true, 21: true, 24: true}
147+
if !validCounts[wordCount] {
148+
return fmt.Errorf("invalid mnemonic: expected 12, 15, 18, 21, or 24 words, got %d", wordCount)
149+
}
150+
151+
// Basic character validation (words should be lowercase alphabetic)
152+
for i, word := range words {
153+
if len(word) < 3 || len(word) > 8 {
154+
return fmt.Errorf("invalid word at position %d: '%s' (expected 3-8 characters)", i+1, word)
155+
}
156+
for _, c := range word {
157+
if c < 'a' || c > 'z' {
158+
return fmt.Errorf("invalid character in word at position %d: '%s'", i+1, word)
159+
}
160+
}
161+
}
162+
163+
return nil
164+
}
165+
166+
// ImportKey imports an existing key from a mnemonic phrase
167+
func (s *svc) ImportKey(ctx context.Context, name string, mnemonic string) (KeyInfo, error) {
168+
if name == "" {
169+
return KeyInfo{}, errors.New("key name required")
170+
}
171+
if mnemonic == "" {
172+
return KeyInfo{}, errors.New("mnemonic phrase required")
173+
}
174+
if s.opts.BinPath == "" {
175+
s.opts.BinPath = "pchaind"
176+
}
177+
178+
// Check if key already exists
179+
show := exec.CommandContext(ctx, s.opts.BinPath, "keys", "show", name, "-a", "--keyring-backend", s.opts.Keyring, "--home", s.opts.HomeDir)
180+
if out, err := show.Output(); err == nil {
181+
return KeyInfo{}, fmt.Errorf("key '%s' already exists with address %s", name, strings.TrimSpace(string(out)))
182+
}
183+
184+
// Import key using --recover flag with mnemonic piped via stdin
185+
add := exec.CommandContext(ctx, s.opts.BinPath, "keys", "add", name,
186+
"--recover",
187+
"--keyring-backend", s.opts.Keyring,
188+
"--algo", "eth_secp256k1",
189+
"--home", s.opts.HomeDir)
190+
191+
// Pipe mnemonic to stdin
192+
add.Stdin = strings.NewReader(mnemonic + "\n")
193+
194+
output, err := add.CombinedOutput()
195+
if err != nil {
196+
// Check for common error patterns
197+
outStr := string(output)
198+
if strings.Contains(outStr, "invalid mnemonic") || strings.Contains(outStr, "invalid checksum") {
199+
return KeyInfo{}, errors.New("invalid mnemonic phrase: checksum verification failed")
200+
}
201+
return KeyInfo{}, fmt.Errorf("key import failed: %w\nOutput: %s", err, outStr)
202+
}
203+
204+
// Get the address of the imported key
205+
out2, err := exec.CommandContext(ctx, s.opts.BinPath, "keys", "show", name, "-a", "--keyring-backend", s.opts.Keyring, "--home", s.opts.HomeDir).Output()
206+
if err != nil {
207+
return KeyInfo{}, fmt.Errorf("failed to get imported key address: %w", err)
208+
}
209+
210+
addr := strings.TrimSpace(string(out2))
211+
// Note: We don't return mnemonic for imported keys (user already has it)
212+
return s.getKeyInfo(ctx, name, addr, "")
213+
}
214+
139215
func (s *svc) GetEVMAddress(ctx context.Context, addr string) (string, error) {
140216
if addr == "" {
141217
return "", errors.New("address required")

0 commit comments

Comments
 (0)