diff --git a/README.md b/README.md index 9ae1b78..73e9e66 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,7 @@ Check README for details on json file format. 21. Vote on a governance proposal on https://snapshot.org ./hmy governance vote-proposal --space=[harmony-mainnet.eth] \ --proposal= --proposal-type=[single-choice] \ - --choice= --app=[APP] --key= \ - --privacy=[PRIVACY TYPE] + --choice= --app=[APP] --key= PS: key must first use (hmy keys import-private-key) to import 22. Enter Console diff --git a/cmd/subcommands/governance.go b/cmd/subcommands/governance.go index 061bd79..74401d1 100644 --- a/cmd/subcommands/governance.go +++ b/cmd/subcommands/governance.go @@ -35,8 +35,9 @@ func commandVote() (cmd *cobra.Command) { var choice string var key string var proposalType string - var privacy string + // var privacy string var app string + var reason string cmd = &cobra.Command{ Use: "vote-proposal", @@ -63,9 +64,10 @@ func commandVote() (cmd *cobra.Command) { Proposal: proposal, ProposalType: proposalType, Choice: choice, - Privacy: privacy, + // Privacy: privacy, App: app, From: account.Address.Hex(), + Reason: reason, }) }, } @@ -75,8 +77,9 @@ func commandVote() (cmd *cobra.Command) { cmd.Flags().StringVar(&proposal, "proposal", "", "Proposal hash") cmd.Flags().StringVar(&proposalType, "proposal-type", "single-choice", "Proposal type like single-choice, approval, quadratic, etc.") cmd.Flags().StringVar(&choice, "choice", "", "Vote choice either as integer, list of integers (e.x. when using ranked choice voting), or string") - cmd.Flags().StringVar(&privacy, "privacy", "", "Vote privacy ex. shutter") - cmd.Flags().StringVar(&app, "app", "", "Voting app") + // cmd.Flags().StringVar(&privacy, "privacy", "", "Vote privacy e.x. shutter") + cmd.Flags().StringVar(&app, "app", "snapshot", "Voting app") + cmd.Flags().StringVar(&reason, "reason", "", "Reason for your choice") cmd.Flags().BoolVar(&userProvidesPassphrase, "passphrase", false, ppPrompt) cmd.MarkFlagRequired("key") diff --git a/cmd/subcommands/values.go b/cmd/subcommands/values.go index e1ab1a8..9e91464 100644 --- a/cmd/subcommands/values.go +++ b/cmd/subcommands/values.go @@ -107,8 +107,7 @@ Check README for details on json file format. %s ./hmy governance vote-proposal --space=[harmony-mainnet.eth] \ --proposal= --proposal-type=[single-choice] \ - --choice= --app=[APP] --key= \ - --privacy=[PRIVACY TYPE] + --choice= --app=[APP] --key= PS: key must first use (hmy keys import-private-key) to import %s diff --git a/pkg/governance/eip712.go b/pkg/governance/eip712.go index d5dcdfe..500197b 100644 --- a/pkg/governance/eip712.go +++ b/pkg/governance/eip712.go @@ -144,18 +144,32 @@ func (typedData *TypedData) String() (string, error) { "proposal": typedData.Message["proposal"], "choice": typedData.Message["choice"], "app": typedData.Message["app"], + "reason": typedData.Message["reason"], // this conversion is required to stop snapshot // from complaining about `wrong envelope format` "timestamp": ts, "from": typedData.Message["from"], }, } + // same comment as above if typedData.Types["Vote"][4].Type == "uint32" { if choice, err := toUint64(typedData.Message["choice"]); err != nil { return "", errors.Wrapf(err, "choice") } else { formatted.Message["choice"] = choice } + // prevent hex choice interpretation + } else if typedData.Types["Vote"][4].Type == "uint32[]" { + arr := typedData.Message["choice"].([]interface{}) + res := make([]uint64, len(arr)) + for i, a := range arr { + if c, err := toUint64(a); err != nil { + return "", errors.Wrapf(err, "choice member %d", i) + } else { + res[i] = c + } + } + formatted.Message["choice"] = res } message, err := json.Marshal(formatted) if err != nil { diff --git a/pkg/governance/types.go b/pkg/governance/types.go index d8d969e..43aed0d 100644 --- a/pkg/governance/types.go +++ b/pkg/governance/types.go @@ -13,14 +13,23 @@ import ( "github.com/pkg/errors" ) +var ( + voteToNumberMapping = map[string]int64{ + "for": 1, + "against": 2, + "abstain": 3, + } +) + type Vote struct { From string // --key Space string // --space Proposal string // --proposal ProposalType string // --proposal-type Choice string // --choice - Privacy string // --privacy + // Privacy string // --privacy App string // --app + Reason string // --reason Timestamp int64 // not exposed to the end user } @@ -65,7 +74,7 @@ func (v *Vote) ToEIP712() (*TypedData, error) { proposal = v.Proposal } - // vote type, vote choice and vote privacy + // vote type, vote choice and vote privacy (TODO) // choice needs to be converted into its native format for envelope var choice interface{} // The space between [1, 2, 3] does not matter since we parse it @@ -74,7 +83,6 @@ func (v *Vote) ToEIP712() (*TypedData, error) { // --proposal 0xTruncated \ // --proposal-type {"approval","ranked-choice"} \ // --choice "[1, 2, 3]" \ - // --app my-app \ // --key if v.ProposalType == "approval" || v.ProposalType == "ranked-choice" { myType = append(myType, eip712.Type{ @@ -93,14 +101,15 @@ func (v *Vote) ToEIP712() (*TypedData, error) { "unexpected value of choice %s (expected uint32[])", choice, ) } - // The space between [1, 2, 3] does not matter to snapshot.org + // The space between --choice {value} does not matter to snapshot.org // But for comparing with the snapshot-js library, remove it // hmy governance vote-proposal \ // --space harmony-mainnet.eth \ // --proposal 0xTruncated \ + // # either quadratic or weighted // --proposal-type {"quadratic","weighted"} \ - // --choice "[1,2,3]" \ - // --app my-app \ + // # 20, 20, 40 of my vote (total 80) goes to 1, 2, 3 - note the single / double quotes + // --choice '{"1":20,"2":20,"3":40}' \ // --key } else if v.ProposalType == "quadratic" || v.ProposalType == "weighted" { myType = append(myType, eip712.Type{ @@ -108,26 +117,25 @@ func (v *Vote) ToEIP712() (*TypedData, error) { Type: "string", }) choice = v.Choice + // TODO Untested // hmy governance vote-proposal \ // --space harmony-mainnet.eth \ // --proposal 0xTruncated \ // --proposal-type ANY \ // --choice "unknown-format" \ - // --app my-app \ // --key - // --privacy shutter - } else if v.Privacy == "shutter" { - myType = append(myType, eip712.Type{ - Name: "choice", - Type: "string", - }) - choice = v.Choice + // --privacy shutter + // } else if v.Privacy == "shutter" { + // myType = append(myType, eip712.Type{ + // Name: "choice", + // Type: "string", + // }) + // choice = v.Choice // hmy governance vote-proposal \ // --space harmony-mainnet.eth \ // --proposal 0xTruncated \ // --proposal-type single-choice \ // --choice 1 \ - // --app my-app \ // --key } else if v.ProposalType == "single-choice" { myType = append(myType, eip712.Type{ @@ -141,6 +149,28 @@ func (v *Vote) ToEIP712() (*TypedData, error) { } else { choice = math.NewHexOrDecimal256(int64(x)) } + // hmy governance vote-proposal \ + // --space harmony-mainnet.eth \ + // --proposal 0xTruncated \ + // --proposal-type basic \ + // # any character case works + // --choice {aBstAin/agAiNst/for} \ + // --key + } else if v.ProposalType == "basic" { + myType = append(myType, eip712.Type{ + Name: "choice", + Type: "uint32", + }) + if number, ok := voteToNumberMapping[strings.ToLower(v.Choice)]; ok { + choice = math.NewHexOrDecimal256(number) + } else { + return nil, errors.New( + fmt.Sprintf( + "unknown basic choice %s", + v.Choice, + ), + ) + } } else { return nil, errors.New( fmt.Sprintf( @@ -150,11 +180,16 @@ func (v *Vote) ToEIP712() (*TypedData, error) { ) } - // order matters so this is added last + // order matters so these are added last + myType = append(myType, eip712.Type{ + Name: "reason", + Type: "string", + }) myType = append(myType, eip712.Type{ Name: "app", Type: "string", }) + // metadata is skipped in this code intentionally if v.Timestamp == 0 { v.Timestamp = time.Now().Unix() @@ -186,6 +221,7 @@ func (v *Vote) ToEIP712() (*TypedData, error) { "timestamp": math.NewHexOrDecimal256(v.Timestamp), "proposal": proposal, "choice": choice, + "reason": v.Reason, "app": v.App, }, PrimaryType: "Vote",