|
| 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