Merge pull request #290 from MaxMustermann2/snapshot-org-test
governance: add voting support for snapshot.orgdependabot/go_modules/github.com/valyala/fasthttp-1.34.0 v1.3.0
commit
dbd9901df9
@ -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() |
||||
} |
@ -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…
Reference in new issue