Merge pull request #290 from MaxMustermann2/snapshot-org-test

governance: add voting support for snapshot.org
dependabot/go_modules/github.com/valyala/fasthttp-1.34.0 v1.3.0
harmony-devops 2 years ago committed by GitHub
commit dbd9901df9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      Makefile
  2. 35
      README.md
  3. 115
      cmd/subcommands/governance.go
  4. 37
      cmd/subcommands/values.go
  5. 21
      go.mod
  6. 717
      go.sum
  7. 238
      pkg/governance/api.go
  8. 278
      pkg/governance/cli.go
  9. 17
      pkg/governance/config.go
  10. 162
      pkg/governance/eip712.go
  11. 35
      pkg/governance/request.go
  12. 69
      pkg/governance/signing.go
  13. 71
      pkg/governance/signing_test.go
  14. 44
      pkg/governance/tool.go
  15. 152
      pkg/governance/types.go
  16. 14
      pkg/governance/util.go

@ -25,7 +25,7 @@ all:
static:
make -C $(shell go env GOPATH)/src/github.com/harmony-one/mcl
make -C $(shell go env GOPATH)/src/github.com/harmony-one/bls minimised_static BLS_SWAP_G=1
source $(shell go env GOPATH)/src/github.com/harmony-one/harmony/scripts/setup_bls_build_flags.sh && $(env) go build -o $(cli) -ldflags="$(ldflags) -w -extldflags \"-static\"" cmd/main.go
source $(shell go env GOPATH)/src/github.com/harmony-one/harmony/scripts/setup_bls_build_flags.sh && $(env) go build -o $(cli) -ldflags="$(ldflags) -w -extldflags \"-static -z muldefs\"" cmd/main.go
cp $(cli) hmy
debug:

@ -126,36 +126,15 @@ Check README for details on json file format.
20. Check which shard your BLS public key would be assigned to as a validator
./hmy --node=https://api.s0.t.hmny.io utility shard-for-bls <BLS_PUBLIC_KEY>
21. List Space In Governance
./hmy governance list-space
22. List Proposal In Space Of Governance
./hmy governance list-proposal --space=[space key, example: staking-testnet]
23. View Proposal In Governance
./hmy governance view-proposal --proposal=[proposal hash]
24. New Proposal In Space Of Governance
./hmy governance new-proposal --proposal-yaml=[file path] --key=[key name]
PS: key must first use (hmy keys import-private-key) to import
Yaml example(time is in UTC timezone):
space: staking-testnet
start: 2020-04-16 21:45:12
end: 2020-04-21 21:45:12
choices:
- yes
- no
title: this is title
body: |
this is body,
you can write mutli line
25. Vote Proposal In Space Of Governance
./hmy governance vote-proposal --proposal=[proposal hash] --choice=[your choice text, eg: yes] --key=[key name]
21. Vote on a governance proposal on https://snapshot.org
./hmy governance vote-proposal --space=[harmony-mainnet.eth] \
--proposal=<PROPOSAL_IPFS_HASH> --proposal-type=[single-choice] \
--choice=<VOTING_CHOICE(S)> --app=[APP] --key=<ACCOUNT_ADDRESS_OR_NAME> \
--privacy=[PRIVACY TYPE]
PS: key must first use (hmy keys import-private-key) to import
26. Enter Console
./hmy command --net=testnet --shard=0
22. Enter Console
./hmy command --net=testnet
```
# Sending batched transactions

@ -2,6 +2,7 @@ package cmd
import (
"fmt"
"github.com/harmony-one/go-sdk/pkg/governance"
"github.com/harmony-one/go-sdk/pkg/store"
"github.com/harmony-one/harmony/accounts"
@ -11,9 +12,9 @@ import (
func init() {
cmdGovernance := &cobra.Command{
Use: "governance",
Short: "Support interaction with the Harmony governance app.",
Short: "Interact with the Harmony spaces on https://snapshot.org",
Long: `
Support interaction with the Harmony governance app, especially for validators that do not want to import their account private key into either metamask or onewallet.
Support interaction with the Harmony governance space on Snapshot, especially for validators that do not want to import their account private key into either metamask or onewallet.
`,
RunE: func(cmd *cobra.Command, args []string) error {
cmd.Help()
@ -22,97 +23,20 @@ Support interaction with the Harmony governance app, especially for validators t
}
cmdGovernance.AddCommand([]*cobra.Command{
{
Use: "list-space",
Short: "List all spaces of the governance app",
RunE: func(cmd *cobra.Command, args []string) error {
return governance.PrintListSpace()
},
},
commandListProposal(),
commandViewProposal(),
commandNewProposal(),
commandVote(),
}...)
RootCmd.AddCommand(cmdGovernance)
}
func commandListProposal() (cmd *cobra.Command) {
var space string
cmd = &cobra.Command{
Use: "list-proposal",
Short: "List all proposals for the given space",
RunE: func(cmd *cobra.Command, args []string) error {
return governance.PrintListProposals(space)
},
}
cmd.Flags().StringVar(&space, "space", "", "Space the proposal belongs to e.g. 'staking-mainnet'")
cmd.MarkFlagRequired("space")
return
}
func commandViewProposal() (cmd *cobra.Command) {
var proposal string
cmd = &cobra.Command{
Use: "view-proposal",
Short: "View a proposal",
RunE: func(cmd *cobra.Command, args []string) error {
return governance.PrintViewProposal(proposal)
},
}
cmd.Flags().StringVar(&proposal, "proposal", "", "Proposal hash")
cmd.MarkFlagRequired("proposal")
return
}
func commandNewProposal() (cmd *cobra.Command) {
var proposal string
var key string
cmd = &cobra.Command{
Use: "new-proposal",
Short: "Start a new proposal",
RunE: func(cmd *cobra.Command, args []string) error {
keyStore := store.FromAccountName(key)
passphrase, err := getPassphrase()
if err != nil {
return err
}
if len(keyStore.Accounts()) <= 0 {
return fmt.Errorf("Couldn't find address from the key")
}
account := accounts.Account{Address: keyStore.Accounts()[0].Address}
err = keyStore.Unlock(accounts.Account{Address: keyStore.Accounts()[0].Address}, passphrase)
if err != nil {
return err
}
return governance.NewProposal(keyStore, account, proposal)
},
}
cmd.Flags().StringVar(&proposal, "proposal-yaml", "", "Proposal yaml path")
cmd.Flags().StringVar(&key, "key", "", "Account address. Must first use (hmy keys import-private-key) to import.")
cmd.Flags().BoolVar(&userProvidesPassphrase, "passphrase", false, ppPrompt)
cmd.MarkFlagRequired("proposal-yaml")
cmd.MarkFlagRequired("key")
return
}
func commandVote() (cmd *cobra.Command) {
var space string
var proposal string
var choice string
var key string
var proposalType string
var privacy string
var app string
cmd = &cobra.Command{
Use: "vote-proposal",
@ -125,7 +49,7 @@ func commandVote() (cmd *cobra.Command) {
}
if len(keyStore.Accounts()) <= 0 {
return fmt.Errorf("Couldn't find address from the key")
return fmt.Errorf("couldn't find address from the key")
}
account := accounts.Account{Address: keyStore.Accounts()[0].Address}
@ -134,16 +58,29 @@ func commandVote() (cmd *cobra.Command) {
return err
}
return governance.Vote(keyStore, account, proposal, choice)
return governance.DoVote(keyStore, account, governance.Vote{
Space: space,
Proposal: proposal,
ProposalType: proposalType,
Choice: choice,
Privacy: privacy,
App: app,
From: account.Address.Hex(),
})
},
}
cmd.Flags().StringVar(&key, "key", "", "Account name. Must first use (hmy keys import-private-key) to import.")
cmd.Flags().StringVar(&space, "space", "harmony-mainnet.eth", "Snapshot space")
cmd.Flags().StringVar(&proposal, "proposal", "", "Proposal hash")
cmd.Flags().StringVar(&choice, "choice", "", "Vote choice e.g. 'agree' or 'disagree'")
cmd.Flags().StringVar(&key, "key", "", "Account address. Must first use (hmy keys import-private-key) to import.")
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().BoolVar(&userProvidesPassphrase, "passphrase", false, ppPrompt)
cmd.MarkFlagRequired("proposal")
cmd.MarkFlagRequired("choose")
cmd.MarkFlagRequired("key")
cmd.MarkFlagRequired("proposal")
cmd.MarkFlagRequired("choice")
return
}

@ -105,32 +105,14 @@ Check README for details on json file format.
./hmy --node=[NODE] utility shard-for-bls <BLS_PUBLIC_KEY>
%s
./hmy governance list-space
%s
./hmy governance list-proposal --space=[space key, example: staking-testnet]
%s
./hmy governance view-proposal --proposal=[proposal hash]
./hmy governance vote-proposal --space=[harmony-mainnet.eth] \
--proposal=<PROPOSAL_IPFS_HASH> --proposal-type=[single-choice] \
--choice=<VOTING_CHOICE(S)> --app=[APP] --key=<ACCOUNT_ADDRESS_OR_NAME> \
--privacy=[PRIVACY TYPE]
PS: key must first use (hmy keys import-private-key) to import
%s
./hmy governance new-proposal --proposal-yaml=[file path] --key=[account address]
PS: key must first use (hmy keys import-private-key) to import
Yaml example(time is in UTC timezone):
space: staking-testnet
start: 2020-04-16 21:45:12
end: 2020-04-21 21:45:12
choices:
- yes
- no
title: this is title
body: |
this is body,
you can write mutli line
%s
./hmy governance vote-proposal --proposal=[proposal hash] --choice=[your choise text, eg: yes] --key=[account address]
PS: key must first use (hmy keys import-private-key) to import
./hmy command --net=testnet
`,
g("1. Check account balance on given chain"),
g("2. Check sent transaction"),
@ -152,10 +134,7 @@ PS: key must first use (hmy keys import-private-key) to import
g("18. Get current staking utility metrics"),
g("19. Check in-memory record of failed staking transactions"),
g("20. Check which shard your BLS public key would be assigned to as a validator"),
g("21. List Space In Governance"),
g("22. List Proposal In Space Of Governance"),
g("23. View Proposal In Governance"),
g("24. New Proposal In Space Of Governance"),
g("25. Vote Proposal In Space Of Governance"),
g("21. Vote on a governance proposal on https://snapshot.org"),
g("22. Enter console"),
)
)

@ -4,41 +4,28 @@ go 1.14
require (
github.com/aristanetworks/goarista v0.0.0-20191023202215-f096da5361bb // indirect
github.com/btcsuite/btcd v0.21.0-beta
github.com/btcsuite/btcutil v1.0.2
github.com/btcsuite/btcd v0.22.1
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce
github.com/cosmos/cosmos-sdk v0.37.0
github.com/davidlazar/go-crypto v0.0.0-20190912175916-7055855a373f // indirect
github.com/deckarep/golang-set v1.7.1
github.com/dop251/goja v0.0.0-20210427212725-462d53687b0d
github.com/ethereum/go-ethereum v1.9.23
github.com/fatih/color v1.9.0
github.com/golang/snappy v0.0.2-0.20200707131729-196ae77b8a26 // indirect
github.com/gorilla/handlers v1.4.0 // indirect
github.com/harmony-one/bls v0.0.7-0.20191214005344-88c23f91a8a9
github.com/harmony-one/harmony v1.10.2-0.20210123081216-6993b9ad0ca1
github.com/iancoleman/strcase v0.0.0-20190422225806-e506e3ef7365 // indirect
github.com/ipfs/go-todocounter v0.0.2 // indirect
github.com/jackpal/gateway v1.0.6 // indirect
github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect
github.com/karalabe/hid v1.0.0
github.com/libp2p/go-libp2p-host v0.1.0 // indirect
github.com/libp2p/go-libp2p-net v0.1.0 // indirect
github.com/libp2p/go-libp2p-routing v0.1.0 // indirect
github.com/libp2p/go-sockaddr v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.9
github.com/mitchellh/go-homedir v1.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v0.0.5
github.com/tyler-smith/go-bip39 v1.0.2
github.com/uber/jaeger-client-go v2.20.1+incompatible // indirect
github.com/uber/jaeger-lib v2.2.0+incompatible // indirect
github.com/valyala/fasthttp v1.2.0
github.com/valyala/fastjson v1.6.3
github.com/wangjia184/sortedset v0.0.0-20160527075905-f5d03557ba30 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
replace github.com/ethereum/go-ethereum => github.com/ethereum/go-ethereum v1.9.9

717
go.sum

File diff suppressed because it is too large Load Diff

@ -2,237 +2,29 @@ package governance
import (
"encoding/json"
"fmt"
"strconv"
"strings"
)
type Space struct {
Name string `json:"name"`
Key string `json:"key,omitempty"`
Symbol string `json:"symbol"`
}
func listSpaces() (spaces map[string]*Space, err error) {
var result map[string]*Space
err = getAndParse(urlListSpace, &result)
if err != nil {
return nil, err
}
for k, space := range result {
space.Key = k
}
return result, nil
}
type ProposalMsgPayload struct {
End float64 `json:"end"`
Body string `json:"body"`
Name string `json:"name"`
Start float64 `json:"start"`
Choices []string `json:"choices"`
Snapshot int `json:"snapshot"`
}
type ProposalMsg struct {
Version string `json:"version"`
Timestamp string `json:"timestamp"`
Space string `json:"space"`
Type string `json:"type"`
Payload ProposalMsgPayload `json:"payload"`
}
type Proposal struct {
Address string `json:"address"`
Msg ProposalMsg `json:"msg"`
Sig string `json:"sig"`
AuthorIpfsHash string `json:"authorIpfsHash"`
RelayerIpfsHash string `json:"relayerIpfsHash"`
}
func listProposalsBySpace(spaceName string) (spaces map[string]*Proposal, err error) {
var result map[string]*Proposal
err = getAndParse(governanceApi(fmt.Sprintf(urlListProposalsBySpace, spaceName)), &result)
if err != nil {
return nil, err
}
return result, nil
}
type ProposalIPFSMsg struct {
Version string `json:"version"`
Timestamp string `json:"timestamp"`
Space string `json:"space"`
Type string `json:"type"`
Payload ProposalMsgPayload `json:"payload"`
}
type ProposalVoteMsgPayload struct {
Choice json.RawMessage `json:"choice"`
Proposal string `json:"proposal"`
}
func (p *ProposalVoteMsgPayload) choices() []int {
var one int
err := json.Unmarshal(p.Choice, &one)
if err == nil {
return []int{one}
}
var many string
err = json.Unmarshal(p.Choice, &many)
if err != nil {
return []int{}
}
splits := strings.Split(many, "-")
ret := make([]int, 0, len(splits))
for _, split := range splits {
number, err := strconv.Atoi(split)
if err != nil {
return []int{}
}
ret = append(ret, number)
}
return ret
}
type ProposalVoteMsg struct {
Version string `json:"version"`
Timestamp string `json:"timestamp"`
Space string `json:"space"`
Type string `json:"type"`
Payload ProposalVoteMsgPayload `json:"payload"`
}
type ProposalVote struct {
Address string `json:"address"`
Msg ProposalVoteMsg `json:"msg"`
Sig string `json:"sig"`
AuthorIpfsHash string `json:"authorIpfsHash"`
RelayerIpfsHash string `json:"relayerIpfsHash"`
}
type ProposalIPFS struct {
Address string `json:"address"`
Msg string `json:"msg"`
Sig string `json:"sig"`
Version string `json:"version"`
parsedMsg *ProposalIPFSMsg
votes map[string]*ProposalVote
}
func viewProposalsByProposalHash(proposalHash string) (proposal *ProposalIPFS, err error) {
var result *ProposalIPFS = &ProposalIPFS{
parsedMsg: &ProposalIPFSMsg{},
votes: make(map[string]*ProposalVote),
}
err = getAndParse(governanceApi(fmt.Sprintf(urlGetProposalInfo, proposalHash)), result)
if err != nil {
return nil, err
}
"github.com/pkg/errors"
)
err = json.Unmarshal([]byte(result.Msg), result.parsedMsg)
func submitMessage(address string, typedData *TypedData, sign string) (map[string]interface{}, error) {
data, err := typedData.String()
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "could not encode EIP712 data")
}
err = getAndParse(governanceApi(fmt.Sprintf(urlListProposalsVoteBySpaceAndProposal, result.parsedMsg.Space, proposalHash)), &result.votes)
if err != nil {
return nil, err
type body struct {
Address string `json:"address"`
Sig string `json:"sig"`
JsonData json.RawMessage `json:"data"`
}
return result, nil
}
type NewProposalJson struct {
Version string `json:"version"`
Timestamp string `json:"timestamp"`
Space string `json:"space"`
Type string `json:"type"`
Payload struct {
Name string `json:"name"`
Body string `json:"body"`
Choices []string `json:"choices"`
Start float64 `json:"start"`
End float64 `json:"end"`
Snapshot int `json:"snapshot"`
Metadata struct {
Strategies []struct {
Name string `json:"name"`
Params struct {
Address string `json:"address"`
Symbol string `json:"symbol"`
Decimals int `json:"decimals"`
} `json:"params"`
} `json:"strategies"`
} `json:"metadata"`
MaxCanSelect int `json:"maxCanSelect"`
} `json:"payload"`
}
type NewProposalResponse struct {
IpfsHash string `json:"ipfsHash"`
}
func submitMessage(address string, content string, sign string) (resp *NewProposalResponse, err error) {
message, err := json.Marshal(map[string]string{
"address": address,
"msg": content,
"sig": sign,
message, err := json.Marshal(body{
Address: address,
Sig: sign,
JsonData: json.RawMessage(data),
})
if err != nil {
return nil, err
}
resp = &NewProposalResponse{}
err = postAndParse(urlMessage, message, resp)
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "could not encode body")
}
return resp, nil
}
type ValidatorsItem struct {
Active bool `json:"active"`
Apr float64 `json:"apr,omitempty"`
Address string `json:"address"`
Name string `json:"name"`
Rate string `json:"rate"`
TotalStake string `json:"total_stake"`
UptimePercentage *float64 `json:"uptime_percentage"`
Identity string `json:"identity"`
HasLogo bool `json:"hasLogo"`
}
type ValidatorsInfo struct {
Validators []ValidatorsItem `json:"validators"`
TotalFound int `json:"totalFound"`
Total int `json:"total"`
TotalActive int `json:"total_active"`
}
func getValidators(url governanceApi) map[string]ValidatorsItem {
info := &ValidatorsInfo{}
err := getAndParse(url, info)
if err != nil {
return nil
}
res := map[string]ValidatorsItem{}
for _, validator := range info.Validators {
res[validator.Address] = validator
}
return res
return postAndParse(urlMessage, message)
}

@ -1,293 +1,27 @@
package governance
import (
"bytes"
"encoding/json"
"fmt"
"github.com/harmony-one/go-sdk/pkg/address"
"github.com/harmony-one/harmony/accounts"
"github.com/harmony-one/harmony/accounts/keystore"
"github.com/olekukonko/tablewriter"
"gopkg.in/yaml.v3"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func PrintListSpace() error {
spaces, err := listSpaces()
if err != nil {
return err
}
table := tablewriter.NewWriter(os.Stdout)
table.SetBorder(false)
table.SetHeader([]string{"Key", "Name"})
for key, space := range spaces {
table.Append([]string{key, space.Name})
}
table.Render()
return nil
}
func PrintListProposals(spaceName string) error {
proposals, err := listProposalsBySpace(spaceName)
if err != nil {
return err
}
table := tablewriter.NewWriter(os.Stdout)
table.SetBorder(false)
table.SetHeader([]string{"Key", "Name", "Start Date", "End Date"})
for key, proposal := range proposals {
table.Append([]string{
key,
proposal.Msg.Payload.Name,
timestampToDateString(proposal.Msg.Payload.Start),
timestampToDateString(proposal.Msg.Payload.End),
})
}
table.Render()
return nil
}
func PrintViewProposal(proposalHash string) error {
proposals, err := viewProposalsByProposalHash(proposalHash)
if err != nil {
return err
}
var validators map[string]ValidatorsItem
switch proposals.parsedMsg.Space {
case "staking-mainnet":
validators = getValidators(urlGetValidatorsInMainNet)
case "staking-testnet":
validators = getValidators(urlGetValidatorsInTestNet)
}
fmt.Printf("Author : %s\n", address.ToBech32(address.Parse(proposals.Address)))
fmt.Printf("IPFS : %s\n", proposalHash)
fmt.Printf("Space : %s\n", proposals.parsedMsg.Space)
fmt.Printf("Start : %s\n", timestampToDateString(proposals.parsedMsg.Payload.Start))
fmt.Printf("End : %s\n", timestampToDateString(proposals.parsedMsg.Payload.End))
fmt.Printf("Choose : %s\n", strings.Join(proposals.parsedMsg.Payload.Choices, " / "))
fmt.Printf("Content: \n")
linePaddingPrint(proposals.parsedMsg.Payload.Body, true)
fmt.Printf("\n")
fmt.Printf("Votes: \n")
var buf bytes.Buffer
table := tablewriter.NewWriter(&buf)
table.SetBorder(false)
table.SetHeader([]string{"Address", "Choose", "Stack"})
for _, vote := range proposals.votes {
stack := "0"
addr := address.ToBech32(address.Parse(vote.Address))
if v, ok := validators[addr]; ok {
float, err := strconv.ParseFloat(v.TotalStake, 64)
if err == nil {
stack = fmt.Sprintf("%.2f", float/1e18)
}
}
choices := make([]string, 0)
for _, choice := range vote.Msg.Payload.choices() {
choices = append(choices, proposals.parsedMsg.Payload.Choices[choice-1])
}
table.Append([]string{
addr,
strings.Join(choices, ", "),
stack,
})
}
table.Render()
linePaddingPrint(buf.String(), false)
return nil
}
type NewProposalYaml struct {
Space string `yaml:"space"`
Start time.Time `yaml:"start"`
End time.Time `yaml:"end"`
Choices []string `yaml:"choices"`
Title string `yaml:"title"`
Body string `yaml:"body"`
Snapshot int `yaml:"snapshot"`
}
var proposalTemplate = []byte(`{
"version": "0.2.0",
"type": "proposal",
"payload": {
"metadata": {
"strategies": [
{
"name": "erc20-balance-of",
"params": {
"address": "0x00eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"symbol": "ONE",
"decimals": 18
}
}
]
},
"maxCanSelect": 1
}
}`)
func NewProposal(keyStore *keystore.KeyStore, account accounts.Account, proposalYamlPath string) error {
proposalYamlFile, err := os.Open(proposalYamlPath)
if err != nil {
return err
}
defer proposalYamlFile.Close()
proposalYaml := &NewProposalYaml{}
err = yaml.NewDecoder(proposalYamlFile).Decode(proposalYaml)
if err != nil {
return err
}
rand.Seed(time.Now().Unix())
proposalJson := &NewProposalJson{}
err = json.Unmarshal(proposalTemplate, proposalJson)
if err != nil {
return err
}
proposalJson.Space = proposalYaml.Space
proposalJson.Timestamp = fmt.Sprintf("%d", time.Now().Unix())
proposalJson.Payload.Name = proposalYaml.Title
proposalJson.Payload.Body = proposalYaml.Body
proposalJson.Payload.Choices = proposalYaml.Choices
proposalJson.Payload.Start = float64(proposalYaml.Start.Unix())
proposalJson.Payload.End = float64(proposalYaml.End.Unix())
proposalJson.Payload.Snapshot = proposalYaml.Snapshot
if !checkPermission(proposalJson.Space, account) {
return fmt.Errorf("no permission!")
}
proposalJsonData, err := json.Marshal(proposalJson)
if err != nil {
return err
}
sign, err := signMessage(keyStore, account, proposalJsonData)
if err != nil {
return err
}
proposal, err := submitMessage(account.Address.String(), string(proposalJsonData), fmt.Sprintf("0x%x", sign))
func DoVote(keyStore *keystore.KeyStore, account accounts.Account, vote Vote) error {
typedData, err := vote.ToEIP712()
if err != nil {
return err
}
fmt.Printf("IPFS : %s\n", proposal.IpfsHash)
return nil
}
func checkPermission(space string, account accounts.Account) bool {
var validators map[string]ValidatorsItem
switch space {
case "staking-mainnet":
validators = getValidators(urlGetValidatorsInMainNet)
case "staking-testnet":
validators = getValidators(urlGetValidatorsInTestNet)
default:
return true
}
if validators == nil {
fmt.Printf("Unable check permission, maybe the RPC not stable, please try later again")
return false
}
if _, ok := validators[address.ToBech32(account.Address)]; ok {
return true
} else {
return false
}
}
type VoteMessage struct {
Version string `json:"version"`
Timestamp string `json:"timestamp"`
Space string `json:"space"`
Type string `json:"type"`
Payload struct {
Proposal string `json:"proposal"`
Choice int `json:"choice"`
Metadata struct {
} `json:"metadata"`
} `json:"payload"`
}
func Vote(keyStore *keystore.KeyStore, account accounts.Account, proposalHash string, choiceText string) error {
proposals, err := viewProposalsByProposalHash(proposalHash)
if err != nil {
return err
}
if !checkPermission(proposals.parsedMsg.Space, account) {
return fmt.Errorf("no permission!")
}
chooseIndex := -1
for i, choice := range proposals.parsedMsg.Payload.Choices {
if choice == choiceText {
chooseIndex = i + 1
break
}
}
if chooseIndex < 0 {
return fmt.Errorf("error choose, please choose: %s", strings.Join(proposals.parsedMsg.Payload.Choices, " / "))
}
voteJson := &VoteMessage{
Version: "0.2.0",
Timestamp: fmt.Sprintf("%d", time.Now().Unix()),
Space: proposals.parsedMsg.Space,
Type: "vote",
Payload: struct {
Proposal string `json:"proposal"`
Choice int `json:"choice"`
Metadata struct{} `json:"metadata"`
}{
Proposal: proposalHash,
Choice: chooseIndex,
Metadata: struct{}{},
},
}
voteJsonData, err := json.Marshal(voteJson)
if err != nil {
return err
}
sign, err := signMessage(keyStore, account, voteJsonData)
sig, err := signTypedData(keyStore, account, typedData)
if err != nil {
return err
}
proposal, err := submitMessage(account.Address.String(), string(voteJsonData), fmt.Sprintf("0x%x", sign))
result, err := submitMessage(account.Address.String(), typedData, sig)
if err != nil {
return err
}
fmt.Printf("Vote IPFS: %s\n", proposal.IpfsHash)
fmt.Println(indent(result))
return nil
}

@ -1,17 +1,8 @@
package governance
type governanceApi string
const backendAddress = "https://snapshot.hmny.io/api/"
const (
_ governanceApi = ""
urlListSpace = backendAddress + "spaces"
urlListProposalsBySpace = backendAddress + "%s/proposals"
urlListProposalsVoteBySpaceAndProposal = backendAddress + "%s/proposal/%s"
urlMessage = backendAddress + "message"
urlGetValidatorsInTestNet = "https://api.stake.hmny.io/networks/testnet/validators"
urlGetValidatorsInMainNet = "https://api.stake.hmny.io/networks/mainnet/validators"
urlGetProposalInfo = "https://gateway.ipfs.io/ipfs/%s"
backendAddress = "https://hub.snapshot.org/api/"
urlMessage = backendAddress + "msg"
version = "0.1.4"
name = "snapshot"
)

@ -0,0 +1,162 @@
package governance
import (
"bytes"
"encoding/json"
"fmt"
"math/big"
"strings"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/signer/core"
"github.com/pkg/errors"
)
// This embedded type was created to override the EncodeData function
// and remove the validation for a mandatory chain id
type TypedData struct {
core.TypedData
}
// dataMismatchError generates an error for a mismatch between
// the provided type and data
func dataMismatchError(encType string, encValue interface{}) error {
return fmt.Errorf("provided data '%v' doesn't match type '%s'", encValue, encType)
}
// EncodeData generates the following encoding:
// `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)`
//
// each encoded member is 32-byte long
// This method overridden here to remove the validation for mandatory chain id
func (typedData *TypedData) EncodeData(primaryType string, data map[string]interface{}, depth int) (hexutil.Bytes, error) {
// if err := typedData.validate(); err != nil {
// return nil, err
// }
buffer := bytes.Buffer{}
// Verify extra data
if len(typedData.Types[primaryType]) < len(data) {
return nil, errors.New("there is extra data provided in the message")
}
// Add typehash
buffer.Write(typedData.TypeHash(primaryType))
// Add field contents. Structs and arrays have special handlers.
for _, field := range typedData.Types[primaryType] {
encType := field.Type
encValue := data[field.Name]
if encType[len(encType)-1:] == "]" {
arrayValue, ok := encValue.([]interface{})
if !ok {
return nil, dataMismatchError(encType, encValue)
}
arrayBuffer := bytes.Buffer{}
parsedType := strings.Split(encType, "[")[0]
for _, item := range arrayValue {
if typedData.Types[parsedType] != nil {
mapValue, ok := item.(map[string]interface{})
if !ok {
return nil, dataMismatchError(parsedType, item)
}
encodedData, err := typedData.EncodeData(parsedType, mapValue, depth+1)
if err != nil {
return nil, err
}
arrayBuffer.Write(encodedData)
} else {
bytesValue, err := typedData.EncodePrimitiveValue(parsedType, item, depth)
if err != nil {
return nil, err
}
arrayBuffer.Write(bytesValue)
}
}
buffer.Write(crypto.Keccak256(arrayBuffer.Bytes()))
} else if typedData.Types[field.Type] != nil {
mapValue, ok := encValue.(map[string]interface{})
if !ok {
return nil, dataMismatchError(encType, encValue)
}
encodedData, err := typedData.EncodeData(field.Type, mapValue, depth+1)
if err != nil {
return nil, err
}
buffer.Write(crypto.Keccak256(encodedData))
} else {
byteValue, err := typedData.EncodePrimitiveValue(encType, encValue, depth)
if err != nil {
return nil, err
}
buffer.Write(byteValue)
}
}
return buffer.Bytes(), nil
}
type TypedDataMessage = map[string]interface{}
// HashStruct generates a keccak256 hash of the encoding of the provided data
// This method overridden here to allow calling the overriden EncodeData above
func (typedData *TypedData) HashStruct(primaryType string, data TypedDataMessage) (hexutil.Bytes, error) {
encodedData, err := typedData.EncodeData(primaryType, data, 1)
if err != nil {
return nil, err
}
return crypto.Keccak256(encodedData), nil
}
func (typedData *TypedData) String() (string, error) {
type domain struct {
Name string `json:"name"`
Version string `json:"version"`
}
// this data structure created to remove unused fields
// for example, domain type is not sent in post request
// and neither are the blank fields in the domain
type data struct {
Domain domain `json:"domain"`
Types core.Types `json:"types"`
Message core.TypedDataMessage `json:"message"`
}
formatted := data{
Domain: domain{
Name: typedData.Domain.Name,
Version: typedData.Domain.Version,
},
Types: core.Types{
typedData.PrimaryType: typedData.Types[typedData.PrimaryType],
},
Message: core.TypedDataMessage{
"space": typedData.Message["space"],
"proposal": typedData.Message["proposal"],
"choice": toUint64(typedData.Message["choice"]),
"app": typedData.Message["app"],
// this conversion is required to stop snapshot
// from complaining about `wrong envelope format`
"timestamp": toUint64(typedData.Message["timestamp"]),
"from": typedData.Message["from"],
},
}
message, err := json.Marshal(formatted)
if err != nil {
return "", err
} else {
return string(message), nil
}
}
func toUint64(x interface{}) uint64 {
y, ok := x.(*math.HexOrDecimal256)
if !ok {
panic("not a *math.HexOrDecimal256")
}
z := (*big.Int)(y)
return z.Uint64()
}

@ -7,38 +7,37 @@ import (
"io/ioutil"
"net/http"
"github.com/pkg/errors"
"github.com/valyala/fastjson"
)
func getAndParse(url governanceApi, data interface{}) error {
resp, err := http.Get(string(url))
if err != nil {
return err
}
defer resp.Body.Close()
return parseAndUnmarshal(resp, data)
}
func postAndParse(url governanceApi, postData []byte, data interface{}) error {
func postAndParse(url string, postData []byte) (map[string]interface{}, error) {
resp, err := http.Post(string(url), "application/json", bytes.NewReader(postData))
if err != nil {
return err
return nil, errors.Wrapf(err, "could not send post request")
}
defer resp.Body.Close()
return parseAndUnmarshal(resp, data)
return parseAndUnmarshal(resp)
}
func parseAndUnmarshal(resp *http.Response, data interface{}) error {
func parseAndUnmarshal(resp *http.Response) (map[string]interface{}, error) {
bodyData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
return nil, err
}
// the 500 is used for errors like `invalid signature`, `not in voting window`, etc.
if resp.StatusCode != 200 && resp.StatusCode != 500 {
return nil, errors.Errorf("unexpected response code %s\nbody: %s", resp.Status, bodyData)
}
if fastjson.GetString(bodyData, "error") != "" {
return fmt.Errorf("error: %s, %s", fastjson.GetString(bodyData, "error"), fastjson.GetString(bodyData, "error_description"))
return nil, fmt.Errorf("error: %s, %s", fastjson.GetString(bodyData, "error"), fastjson.GetString(bodyData, "error_description"))
}
return json.Unmarshal(bodyData, data)
var result map[string]interface{}
if err := json.Unmarshal(bodyData, &result); err != nil {
return nil, errors.Wrapf(err, "could not decode result from %s", string(bodyData))
} else {
return result, nil
}
}

@ -0,0 +1,69 @@
package governance
import (
"fmt"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/harmony-one/harmony/accounts"
"github.com/harmony-one/harmony/accounts/keystore"
"github.com/harmony-one/harmony/crypto/hash"
"github.com/pkg/errors"
)
func encodeForSigning(typedData *TypedData) ([]byte, error) {
domainSeparator, err := typedData.HashStruct("EIP712Domain", typedData.Domain.Map())
if err != nil {
return nil, errors.Wrapf(
err,
"cannot hash the domain structure",
)
}
typedDataHash, err := typedData.HashStruct(typedData.PrimaryType, typedData.Message)
if err != nil {
return nil, errors.Wrapf(
err,
"cannot hash the structure",
)
}
rawData := []byte(fmt.Sprintf("\x19\x01%s%s", string(domainSeparator), string(typedDataHash)))
return rawData, nil
}
// signTypedData encodes and signs EIP-712 data
// it is copied over here from Geth to use our own keystore implementation
func signTypedData(keyStore *keystore.KeyStore, account accounts.Account, typedData *TypedData) (string, error) {
rawData, err := encodeForSigning(typedData)
if err != nil {
return "", errors.Wrapf(
err,
"cannot encode for signing",
)
}
msgHash := hash.Keccak256Hash(rawData)
sign, err := keyStore.SignHash(account, msgHash.Bytes())
if err != nil {
return "", err
}
if len(sign) != 65 {
return "", fmt.Errorf("sign error")
}
sign[64] += 0x1b
return hexutil.Encode(sign), nil
}
// func signMessage(keyStore *keystore.KeyStore, account accounts.Account, data []byte) (string, error) {
// fullMessage := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
// msgHash := hash.Keccak256Hash([]byte(fullMessage))
// sign, err := keyStore.SignHash(account, msgHash.Bytes())
// if err != nil {
// return "", err
// }
// if len(sign) != 65 {
// return "", fmt.Errorf("sign error")
// }
// sign[64] += 0x1b
// return hexutil.Encode(sign), nil
// }

@ -0,0 +1,71 @@
package governance
import (
"encoding/hex"
"os"
"path"
"testing"
"github.com/btcsuite/btcd/btcec"
"github.com/harmony-one/go-sdk/pkg/common"
"github.com/harmony-one/harmony/accounts"
)
func TestSigning(t *testing.T) {
// make the EIP712 data structure
vote := Vote{
Space: "yam.eth",
Proposal: "0x21ea31e896ec5b5a49a3653e51e787ee834aaf953263144ab936ed756f36609f",
ProposalType: "single-choice",
Choice: "1",
App: "my-app",
Timestamp: 1660909056,
From: "0x9E713963a92c02317A681b9bB3065a8249DE124F",
}
typedData, err := vote.ToEIP712()
if err != nil {
t.Fatal(err)
}
// add a temporary key store with the below private key
location := path.Join(os.TempDir(), "hmy-test")
keyStore := common.KeyStoreForPath(location)
privateKeyBytes, _ := hex.DecodeString("91c8360c4cb4b5fac45513a7213f31d4e4a7bfcb4630e9fbf074f42a203ac0b9")
sk, _ := btcec.PrivKeyFromBytes(btcec.S256(), privateKeyBytes)
passphrase := ""
keyStore.ImportECDSA(sk.ToECDSA(), passphrase)
keyStore.Unlock(accounts.Account{Address: keyStore.Accounts()[0].Address}, passphrase)
account := accounts.Account{
Address: keyStore.Accounts()[0].Address,
}
sign, err := signTypedData(keyStore, account, typedData)
if err != nil {
t.Fatal(err)
}
expectedSig := "0x6b572bacbb44efe75cad5b938a5d4fe64c3495bec28807e78989e3159e11d21d5d5568ffabcf274194830b6cb375355af423995afc7ee290e4a632b12bdbe0cc1b"
if sign != expectedSig {
t.Errorf("invalid sig: got %s but expected %s", sign, expectedSig)
}
os.RemoveAll(location)
}
// The below NodeJS code was used to generate the above signature
// import snapshot from '@snapshot-labs/snapshot.js';
// import { Wallet } from "ethers";
// const hub = 'https://hub.snapshot.org';
// const client = new snapshot.Client712(hub);
// const wallet = new Wallet("91c8360c4cb4b5fac45513a7213f31d4e4a7bfcb4630e9fbf074f42a203ac0b9");
// const receipt = await client.vote(wallet, await wallet.getAddress(), {
// space: 'yam.eth',
// proposal: '0x21ea31e896ec5b5a49a3653e51e787ee834aaf953263144ab936ed756f36609f',
// type: 'single-choice',
// choice: 1,
// app: 'my-app',
// timestamp: 1660909056,
// });
// package.json
// "@snapshot-labs/snapshot.js": "^0.4.18"
// "ethers": "^5.6.9"

@ -1,44 +0,0 @@
package governance
import (
"fmt"
"github.com/harmony-one/harmony/accounts"
"github.com/harmony-one/harmony/accounts/keystore"
"github.com/harmony-one/harmony/crypto/hash"
"strconv"
"strings"
"time"
)
func timestampToDateString(timestamp float64) string {
return time.Unix(int64(timestamp), 0).Format(time.RFC822)
}
func linePaddingPrint(content string, trim bool) {
for _, line := range strings.Split(content, "\n") {
trimLine := line
if trim {
trimLine = strings.TrimSpace(line)
}
if trimLine != "" {
fmt.Printf(" %s\n", trimLine)
}
}
}
func signMessage(keyStore *keystore.KeyStore, account accounts.Account, data []byte) (sign []byte, err error) {
signData := append([]byte("\x19Ethereum Signed Message:\n" + strconv.Itoa(len(data))))
msgHash := hash.Keccak256(append(signData, data...))
sign, err = keyStore.SignHash(account, msgHash)
if err != nil {
return nil, err
}
if len(sign) != 65 {
return nil, fmt.Errorf("sign error")
}
sign[64] += 0x1b
return sign, nil
}

@ -0,0 +1,152 @@
package governance
import (
"encoding/json"
"strconv"
"strings"
"time"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
eip712 "github.com/ethereum/go-ethereum/signer/core"
"github.com/pkg/errors"
)
type Vote struct {
From string // --key
Space string // --space
Proposal string // --proposal
ProposalType string // --proposal-type
Choice string // --choice
Privacy string // --privacy
App string // --app
Timestamp int64 // not exposed to the end user
}
func (v *Vote) ToEIP712() (*TypedData, error) {
// common types regardless of parameters
// key `app` appended later because order matters
myType := []eip712.Type{
{
Name: "from",
Type: "address",
},
{
Name: "space",
Type: "string",
},
{
Name: "timestamp",
Type: "uint64",
},
}
var proposal interface{}
isHex := strings.HasPrefix(v.Proposal, "0x")
if isHex {
myType = append(myType, eip712.Type{
Name: "proposal",
Type: "bytes32",
})
if proposalBytes, err := hexutil.Decode(v.Proposal); err != nil {
return nil, errors.Wrapf(
err, "invalid proposal hash %s", v.Proposal,
)
} else {
// EncodePrimitiveValue accepts only hexutil.Bytes not []byte
proposal = hexutil.Bytes(proposalBytes)
}
} else {
myType = append(myType, eip712.Type{
Name: "proposal",
Type: "string",
})
proposal = v.Proposal
}
// vote type, vote choice and vote privacy
// choice needs to be converted into its native format for envelope
var choice interface{}
if v.ProposalType == "approval" || v.ProposalType == "ranked-choice" {
myType = append(myType, eip712.Type{
Name: "choice",
Type: "uint32[]",
})
var is []int
if err := json.Unmarshal([]byte(v.Choice), &is); err == nil {
choice = is
} else {
return nil, errors.Wrapf(err,
"unexpected value of choice %s (expected uint32[])", choice,
)
}
} else if v.ProposalType == "quadratic" || v.ProposalType == "weighted" {
myType = append(myType, eip712.Type{
Name: "choice",
Type: "string",
})
choice = v.Choice
} else if v.Privacy == "shutter" {
myType = append(myType, eip712.Type{
Name: "choice",
Type: "string",
})
choice = v.Choice
} else {
myType = append(myType, eip712.Type{
Name: "choice",
Type: "uint32",
})
if x, err := strconv.Atoi(v.Choice); err != nil {
return nil, errors.Wrapf(err,
"unexpected value of choice %s (expected uint32)", choice,
)
} else {
choice = math.NewHexOrDecimal256(int64(x))
}
}
// order matters so this is added last
myType = append(myType, eip712.Type{
Name: "app",
Type: "string",
})
if v.Timestamp == 0 {
v.Timestamp = time.Now().Unix()
}
typedData := TypedData{
eip712.TypedData{
Domain: eip712.TypedDataDomain{
Name: name,
Version: version,
},
Types: eip712.Types{
"EIP712Domain": {
{
Name: "name",
Type: "string",
},
{
Name: "version",
Type: "string",
},
},
"Vote": myType,
},
Message: eip712.TypedDataMessage{
"from": v.From,
"space": v.Space,
// EncodePrimitiveValue accepts string, float64, or this type
"timestamp": math.NewHexOrDecimal256(v.Timestamp),
"proposal": proposal,
"choice": choice,
"app": v.App,
},
PrimaryType: "Vote",
},
}
return &typedData, nil
}

@ -0,0 +1,14 @@
package governance
import (
"encoding/json"
"fmt"
)
func indent(v interface{}) string {
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
return fmt.Sprintf("%#v", v)
}
return string(b)
}
Loading…
Cancel
Save