Skip to content

Commit ea89cac

Browse files
committed
fix: added governance support
1 parent 2dc466d commit ea89cac

File tree

15 files changed

+1985
-0
lines changed

15 files changed

+1985
-0
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ push-validator withdraw-rewards # Withdraw validator rewards and commission
7474
push-validator restake-rewards # Auto-withdraw and restake all rewards to increase validator power
7575
```
7676

77+
### Governance
78+
```bash
79+
push-validator proposals # List governance proposals (filter with --status voting|passed|rejected)
80+
push-validator vote <id> <option> # Vote on a proposal (yes|no|abstain|no_with_veto)
81+
```
82+
7783
### Monitoring
7884
```bash
7985
push-validator sync # Monitor sync progress
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sort"
7+
"strconv"
8+
"strings"
9+
"time"
10+
11+
"github.com/spf13/cobra"
12+
13+
ui "github.com/pushchain/push-validator-cli/internal/ui"
14+
"github.com/pushchain/push-validator-cli/internal/validator"
15+
)
16+
17+
var flagProposalStatus string
18+
19+
func init() {
20+
proposalsCmd := &cobra.Command{
21+
Use: "proposals",
22+
Short: "List governance proposals",
23+
Long: "List all governance proposals on the Push Chain, optionally filtered by status",
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
return handleProposals(newDeps(), flagOutput == "json")
26+
},
27+
}
28+
proposalsCmd.Flags().StringVar(&flagProposalStatus, "status", "", "Filter by status: voting, passed, rejected, deposit")
29+
rootCmd.AddCommand(proposalsCmd)
30+
}
31+
32+
func handleProposals(d *Deps, jsonOut bool) error {
33+
cfg := d.Cfg
34+
35+
// For JSON output, query raw data directly
36+
if jsonOut {
37+
remote := fmt.Sprintf("https://%s", cfg.GenesisDomain)
38+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
39+
defer cancel()
40+
41+
args := []string{"query", "gov", "proposals", "--node", remote, "-o", "json"}
42+
if flagProposalStatus != "" {
43+
args = append(args, "--status", mapStatusFlag(flagProposalStatus))
44+
}
45+
46+
output, err := d.Runner.Run(ctx, findPchaind(), args...)
47+
if err != nil {
48+
p := getPrinter()
49+
if ctx.Err() == context.DeadlineExceeded {
50+
p.JSON(map[string]any{"ok": false, "error": "timeout connecting to network"})
51+
return silentErr{fmt.Errorf("timeout")}
52+
}
53+
p.JSON(map[string]any{"ok": false, "error": "failed to fetch proposals"})
54+
return silentErr{fmt.Errorf("failed to fetch proposals")}
55+
}
56+
fmt.Println(string(output))
57+
return nil
58+
}
59+
60+
// For table output, use cached fetcher
61+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
62+
defer cancel()
63+
64+
propList, err := d.Fetcher.GetProposals(ctx, cfg)
65+
if err != nil {
66+
c := ui.NewColorConfig()
67+
fmt.Println()
68+
fmt.Println(c.Error(c.Emoji("❌") + " Failed to fetch proposals"))
69+
fmt.Println()
70+
fmt.Println(c.Info("Check your network connection and try again"))
71+
fmt.Println()
72+
return silentErr{fmt.Errorf("failed to fetch proposals")}
73+
}
74+
75+
if propList.Total == 0 {
76+
c := ui.NewColorConfig()
77+
fmt.Println()
78+
fmt.Println(c.Header(" 📜 Governance Proposals "))
79+
fmt.Println(c.Info("No proposals found"))
80+
return nil
81+
}
82+
83+
// Filter by status if specified
84+
filtered := propList.Proposals
85+
if flagProposalStatus != "" {
86+
statusFilter := strings.ToUpper(flagProposalStatus)
87+
filtered = make([]validator.Proposal, 0)
88+
for _, p := range propList.Proposals {
89+
if strings.EqualFold(p.Status, statusFilter) {
90+
filtered = append(filtered, p)
91+
}
92+
}
93+
}
94+
95+
// Sort by voting end date descending (most recent first), then by ID
96+
sort.Slice(filtered, func(i, j int) bool {
97+
// Parse voting end times
98+
var timeI, timeJ time.Time
99+
if filtered[i].VotingEnd != "" {
100+
timeI, _ = time.Parse(time.RFC3339, filtered[i].VotingEnd)
101+
}
102+
if filtered[j].VotingEnd != "" {
103+
timeJ, _ = time.Parse(time.RFC3339, filtered[j].VotingEnd)
104+
}
105+
106+
// If both have dates, sort by date descending
107+
if !timeI.IsZero() && !timeJ.IsZero() {
108+
return timeI.After(timeJ)
109+
}
110+
// If only one has a date, prioritize the one with a date
111+
if !timeI.IsZero() {
112+
return true
113+
}
114+
if !timeJ.IsZero() {
115+
return false
116+
}
117+
// If neither has a date, sort by ID descending
118+
idI, _ := strconv.Atoi(filtered[i].ID)
119+
idJ, _ := strconv.Atoi(filtered[j].ID)
120+
return idI > idJ
121+
})
122+
123+
c := ui.NewColorConfig()
124+
fmt.Println()
125+
fmt.Println(c.Header(" 📜 Governance Proposals "))
126+
127+
if len(filtered) == 0 {
128+
fmt.Println(c.Info("No proposals match the filter"))
129+
return nil
130+
}
131+
132+
headers := []string{"ID", "TITLE", "STATUS", "VOTING ENDS"}
133+
rows := make([][]string, 0, len(filtered))
134+
135+
for _, p := range filtered {
136+
// Truncate title if too long
137+
title := p.Title
138+
if len(title) > 40 {
139+
title = title[:37] + "..."
140+
}
141+
142+
// Format voting end time
143+
votingEnd := "—"
144+
if p.VotingEnd != "" {
145+
if t, err := time.Parse(time.RFC3339, p.VotingEnd); err == nil {
146+
votingEnd = t.Format("2006-01-02 15:04")
147+
}
148+
}
149+
150+
// Color status based on state
151+
status := p.Status
152+
switch p.Status {
153+
case "VOTING":
154+
status = c.Warning(p.Status)
155+
case "PASSED":
156+
status = c.Success(p.Status)
157+
case "REJECTED", "FAILED":
158+
status = c.Error(p.Status)
159+
case "DEPOSIT":
160+
status = c.Info(p.Status)
161+
}
162+
163+
rows = append(rows, []string{
164+
p.ID,
165+
title,
166+
status,
167+
votingEnd,
168+
})
169+
}
170+
171+
fmt.Print(ui.Table(c, headers, rows, nil))
172+
fmt.Printf("Total Proposals: %d\n", len(filtered))
173+
174+
// Show tip about voting if there are voting proposals
175+
hasVoting := false
176+
for _, p := range filtered {
177+
if p.Status == "VOTING" {
178+
hasVoting = true
179+
break
180+
}
181+
}
182+
if hasVoting {
183+
fmt.Println(c.Info("💡 Tip: Use 'push-validator vote <id> <yes|no|abstain|no_with_veto>' to vote"))
184+
}
185+
186+
return nil
187+
}
188+
189+
// mapStatusFlag maps user-friendly status names to chain status values
190+
func mapStatusFlag(status string) string {
191+
switch strings.ToLower(status) {
192+
case "voting":
193+
return "voting_period"
194+
case "passed":
195+
return "passed"
196+
case "rejected":
197+
return "rejected"
198+
case "deposit":
199+
return "deposit_period"
200+
default:
201+
return status
202+
}
203+
}

0 commit comments

Comments
 (0)