feat(cmd): add governance cmd

This commit is contained in:
dawn-to-dusk 2021-03-19 09:51:28 +08:00
parent 2902f16647
commit 0765965e0c
5 changed files with 465 additions and 23 deletions

View File

@ -22,7 +22,7 @@ func Start(config *repo.Config) error {
mux := runtime.NewServeMux(
runtime.WithMarshalerOption(runtime.MIMEWildcard,
&runtime.JSONPb{OrigName: true, EmitDefaults: true},
&runtime.JSONPb{OrigName: true, EmitDefaults: true, EnumsAsInts: true},
),
)

View File

@ -25,6 +25,7 @@ var clientCMD = cli.Command{
txCMD(),
validatorsCMD(),
delVPNodeCMD(),
governanceCMD(),
},
}

View File

@ -0,0 +1,412 @@
package client
import (
"encoding/json"
"fmt"
"strconv"
"time"
"github.com/Rican7/retry"
"github.com/Rican7/retry/strategy"
"github.com/cheynewallace/tabby"
"github.com/fatih/color"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
appchainMgr "github.com/meshplus/bitxhub-core/appchain-mgr"
"github.com/meshplus/bitxhub-model/constant"
"github.com/meshplus/bitxhub-model/pb"
"github.com/meshplus/bitxhub/internal/executor/contracts"
"github.com/meshplus/bitxhub/internal/repo"
"github.com/tidwall/gjson"
"github.com/urfave/cli"
)
func governanceCMD() cli.Command {
return cli.Command{
Name: "governance",
Usage: "governance command",
Subcommands: cli.Commands{
cli.Command{
Name: "vote",
Usage: "vote to a proposal",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Usage: "proposal id",
Required: true,
},
cli.StringFlag{
Name: "info",
Usage: "voting information, approve or reject",
Required: true,
},
cli.StringFlag{
Name: "reason",
Usage: "reason to vote",
Required: true,
},
},
Action: vote,
},
cli.Command{
Name: "proposals",
Usage: "query proposals based on the condition",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Usage: "proposal id",
Required: false,
},
cli.StringFlag{
Name: "type",
Usage: "proposal type, currently only AppchainMgr is supported",
Required: false,
},
cli.StringFlag{
Name: "status",
Usage: "proposal status, one of proposed, approve or reject",
Required: false,
},
cli.StringFlag{
Name: "from",
Usage: "the address of the account to which the proposal was made",
Required: false,
},
},
Action: getProposals,
},
cli.Command{
Name: "chain",
Usage: "query chain status by chain id",
Flags: []cli.Flag{
cli.StringFlag{
Name: "id",
Usage: "chain id",
Required: true,
},
},
Action: getChainStatusById,
},
},
}
}
func vote(ctx *cli.Context) error {
id := ctx.String("id")
info := ctx.String("info")
reason := ctx.String("reason")
if info != "approve" && info != "reject" {
return fmt.Errorf("the info parameter can only have a value of \"approve\" or \"reject\"")
}
repoRoot, err := repo.PathRootWithDefault(ctx.GlobalString("repo"))
if err != nil {
return err
}
keyPath := repo.GetKeyPath(repoRoot)
resp, err := sendTx(ctx, constant.GovernanceContractAddr.String(), 0, uint64(pb.TransactionData_INVOKE), keyPath, uint64(pb.TransactionData_BVM), "Vote",
pb.String(id), pb.String(info), pb.String(reason))
if err != nil {
return fmt.Errorf("send transaction error: %s", err.Error())
}
hash := gjson.Get(string(resp), "tx_hash").String()
var data []byte
if err = retry.Retry(func(attempt uint) error {
data, err = getTxReceipt(ctx, hash)
if err != nil {
fmt.Println("get transaction receipt error: " + err.Error() + "... retry later")
return err
} else {
m := make(map[string]interface{})
if err := json.Unmarshal(data, &m); err != nil {
fmt.Println("get transaction receipt error: " + err.Error() + "... retry later")
return err
}
if errInfo, ok := m["error"]; ok {
fmt.Println("get transaction receipt error: " + errInfo.(string) + "... retry later")
return fmt.Errorf(errInfo.(string))
}
return nil
}
}, strategy.Wait(500*time.Millisecond),
); err != nil {
fmt.Println("get transaction receipt error: " + err.Error())
}
m := &runtime.JSONPb{OrigName: true, EmitDefaults: true, EnumsAsInts: true}
receipt := &pb.Receipt{}
if err = m.Unmarshal(data, receipt); err != nil {
return fmt.Errorf("jsonpb unmarshal receipt error: %w", err)
}
if receipt.IsSuccess() {
color.Green("vote successfully!\n")
} else {
color.Red("vote error: %s\n", string(receipt.Ret))
}
return nil
}
func getProposals(ctx *cli.Context) error {
id := ctx.String("id")
typ := ctx.String("type")
status := ctx.String("status")
from := ctx.String("from")
if err := checkArgs(id, typ, status, from); err != nil {
return err
}
repoRoot, err := repo.PathRootWithDefault(ctx.GlobalString("repo"))
if err != nil {
return err
}
keyPath := repo.GetKeyPath(repoRoot)
proposals := make([]contracts.Proposal, 0)
if id == "" {
if typ != "" {
proposals, err = getProposalsByConditions(ctx, keyPath, "GetProposalsByTyp", typ)
if err != nil {
return fmt.Errorf("get proposals by type error: %w", err)
}
if len(proposals) == 0 {
status = ""
from = ""
}
}
if status != "" {
proposalsTmp, err := getProposalsByConditions(ctx, keyPath, "GetProposalsByStatus", status)
if err != nil {
return fmt.Errorf("get proposals by status error: %w", err)
}
proposals = getdDuplicateProposals(proposals, proposalsTmp)
if len(proposals) == 0 {
from = ""
}
}
if from != "" {
proposalsTmp, err := getProposalsByConditions(ctx, keyPath, "GetProposalsByFrom", from)
if err != nil {
return fmt.Errorf("get proposals by from error: %w", err)
}
proposals = getdDuplicateProposals(proposals, proposalsTmp)
}
} else {
proposals, err = getProposalsByConditions(ctx, keyPath, "GetProposal", id)
if err != nil {
return fmt.Errorf("get proposals by id error: %w", err)
}
}
printProposal(proposals)
return nil
}
func checkArgs(id, typ, status, from string) error {
if id == "" &&
typ == "" &&
status == "" &&
from == "" {
return fmt.Errorf("input at least one query condition")
}
if typ != "" &&
typ != string(contracts.AppchainMgr) &&
typ != string(contracts.RuleMgr) &&
typ != string(contracts.NodeMgr) &&
typ != string(contracts.ServiceMgr) {
return fmt.Errorf("illegal proposal type")
}
if status != "" &&
status != string(contracts.PROPOSED) &&
status != string(contracts.APPOVED) &&
status != string(contracts.REJECTED) {
return fmt.Errorf("illegal proposal status")
}
return nil
}
func getdDuplicateProposals(ps1, ps2 []contracts.Proposal) []contracts.Proposal {
if len(ps1) == 0 {
return ps2
}
proposals := make([]contracts.Proposal, 0)
for _, p1 := range ps1 {
for _, p2 := range ps2 {
if p1.Id == p2.Id {
proposals = append(proposals, p1)
break
}
}
}
return proposals
}
func getProposalsByConditions(ctx *cli.Context, keyPath string, menthod string, arg string) ([]contracts.Proposal, error) {
resp, err := sendTx(ctx, constant.GovernanceContractAddr.String(), 0, uint64(pb.TransactionData_INVOKE), keyPath, uint64(pb.TransactionData_BVM), menthod,
pb.String(arg))
if err != nil {
return nil, fmt.Errorf("send transaction error: %w", err)
}
hash := gjson.Get(string(resp), "tx_hash").String()
var data []byte
if err = retry.Retry(func(attempt uint) error {
data, err = getTxReceipt(ctx, hash)
if err != nil {
fmt.Println("get transaction receipt error: " + err.Error() + "... retry later")
return err
} else {
m := make(map[string]interface{})
if err := json.Unmarshal(data, &m); err != nil {
fmt.Println("get transaction receipt error: " + err.Error() + "... retry later")
return err
}
if errInfo, ok := m["error"]; ok {
fmt.Println("get transaction receipt error: " + errInfo.(string) + "... retry later")
return fmt.Errorf(errInfo.(string))
}
return nil
}
}, strategy.Wait(500*time.Millisecond),
); err != nil {
fmt.Println("get transaction receipt error: " + err.Error())
}
m := &runtime.JSONPb{OrigName: true, EmitDefaults: true, EnumsAsInts: true}
receipt := &pb.Receipt{}
if err = m.Unmarshal(data, receipt); err != nil {
return nil, fmt.Errorf("jsonpb unmarshal receipt error: %w", err)
}
if receipt.IsSuccess() {
proposals := make([]contracts.Proposal, 0)
if menthod == "GetProposal" {
proposal := contracts.Proposal{}
err = json.Unmarshal(receipt.Ret, &proposal)
if err != nil {
return nil, fmt.Errorf("unmarshal receipt error: %w", err)
}
proposals = append(proposals, proposal)
} else {
err = json.Unmarshal(receipt.Ret, &proposals)
if err != nil {
return nil, fmt.Errorf("unmarshal receipt error: %w", err)
}
}
return proposals, nil
} else {
return nil, fmt.Errorf(string(receipt.Ret))
}
}
func printProposal(proposals []contracts.Proposal) {
var table [][]string
table = append(table, []string{"Id", "Type", "Status", "ApproveNum", "RejectNum", "ElectorateNum", "ThresholdNum", "Des"})
for _, pro := range proposals {
table = append(table, []string{
pro.Id,
string(pro.Typ),
string(pro.Status),
strconv.Itoa(int(pro.ApproveNum)),
strconv.Itoa(int(pro.AgainstNum)),
strconv.Itoa(int(pro.ElectorateNum)),
strconv.Itoa(int(pro.ThresholdNum)),
pro.Des,
})
}
PrintTable(table, true)
}
func PrintTable(rows [][]string, header bool) {
// Print the table
t := tabby.New()
if header {
addRow(t, rows[0], header)
rows = rows[1:]
}
for _, row := range rows {
addRow(t, row, false)
}
t.Print()
}
func addRow(t *tabby.Tabby, rawLine []string, header bool) {
// Convert []string to []interface{}
row := make([]interface{}, len(rawLine))
for i, v := range rawLine {
row[i] = v
}
// Add line to the table
if header {
t.AddHeader(row...)
} else {
t.AddLine(row...)
}
}
func getChainStatusById(ctx *cli.Context) error {
id := ctx.String("id")
repoRoot, err := repo.PathRootWithDefault(ctx.GlobalString("repo"))
if err != nil {
return err
}
keyPath := repo.GetKeyPath(repoRoot)
resp, err := sendTx(ctx, constant.AppchainMgrContractAddr.String(), 0, uint64(pb.TransactionData_INVOKE), keyPath, uint64(pb.TransactionData_BVM), "GetAppchain",
pb.String(id))
if err != nil {
return fmt.Errorf("send transaction error: %s", err.Error())
}
hash := gjson.Get(string(resp), "tx_hash").String()
var data []byte
if err = retry.Retry(func(attempt uint) error {
data, err = getTxReceipt(ctx, hash)
if err != nil {
fmt.Println("get transaction receipt error: " + err.Error() + "... retry later")
return err
} else {
m := make(map[string]interface{})
if err := json.Unmarshal(data, &m); err != nil {
fmt.Println("get transaction receipt error: " + err.Error() + "... retry later")
return err
}
if errInfo, ok := m["error"]; ok {
fmt.Println("get transaction receipt error: " + errInfo.(string) + "... retry later")
return fmt.Errorf(errInfo.(string))
}
return nil
}
}, strategy.Wait(500*time.Millisecond),
); err != nil {
fmt.Println("get transaction receipt error: " + err.Error())
}
m := &runtime.JSONPb{OrigName: true, EmitDefaults: true, EnumsAsInts: true}
receipt := &pb.Receipt{}
if err = m.Unmarshal(data, receipt); err != nil {
return fmt.Errorf("jsonpb unmarshal receipt error: %w", err)
}
if receipt.IsSuccess() {
chain := &appchainMgr.Appchain{}
if err := json.Unmarshal(receipt.Ret, chain); err != nil {
return fmt.Errorf("unmarshal receipt error: %w", err)
}
color.Green("appchain %s is %s", chain.ID, string(chain.Status))
} else {
color.Red("get chain status error: %s\n", string(receipt.Ret))
}
return nil
}

View File

@ -19,12 +19,7 @@ func getReceipt(ctx *cli.Context) error {
return fmt.Errorf("please input transaction hash")
}
url, err := getURL(ctx, "receipt/"+ctx.Args().Get(0))
if err != nil {
return err
}
data, err := httpGet(ctx, url)
data, err := getTxReceipt(ctx, ctx.Args().Get(0))
if err != nil {
return err
}
@ -33,3 +28,17 @@ func getReceipt(ctx *cli.Context) error {
return nil
}
func getTxReceipt(ctx *cli.Context, hash string) ([]byte, error) {
url, err := getURL(ctx, "receipt/"+hash)
if err != nil {
return nil, err
}
data, err := httpGet(ctx, url)
if err != nil {
return nil, err
}
return data, nil
}

View File

@ -87,45 +87,67 @@ func sendTransaction(ctx *cli.Context) error {
keyPath = repo.GetKeyPath(repoRoot)
}
resp, err := sendTx(ctx, toString, amount, txType, keyPath, 0, "")
if err != nil {
return fmt.Errorf("send transaction: %w", err)
}
fmt.Println(string(resp))
return nil
}
func sendTx(ctx *cli.Context, toString string, amount uint64, txType uint64, keyPath string, vmType uint64, method string, args ...*pb.Arg) ([]byte, error) {
key, err := repo.LoadKey(keyPath)
if err != nil {
return fmt.Errorf("wrong key: %w", err)
return nil, fmt.Errorf("wrong key: %w", err)
}
from, err := key.PrivKey.PublicKey().Address()
if err != nil {
return fmt.Errorf("wrong private key: %w", err)
return nil, fmt.Errorf("wrong private key: %w", err)
}
to := types.NewAddressByStr(toString)
invokePayload := &pb.InvokePayload{
Method: method,
Args: args,
}
invokePayloadData, err := invokePayload.Marshal()
if err != nil {
return nil, err
}
data := &pb.TransactionData{
Type: pb.TransactionData_Type(txType),
Amount: amount,
Type: pb.TransactionData_Type(txType),
Amount: amount,
VmType: pb.TransactionData_VMType(vmType),
Payload: invokePayloadData,
}
payload, err := data.Marshal()
if err != nil {
return err
return nil, err
}
getNonceUrl, err := getURL(ctx, fmt.Sprintf("pendingNonce/%s", from.String()))
if err != nil {
return err
return nil, err
}
encodedNonce, err := httpGet(ctx, getNonceUrl)
if err != nil {
return err
return nil, err
}
ret, err := parseResponse(encodedNonce)
if err != nil {
return err
return nil, err
}
nonce, err := strconv.ParseUint(ret, 10, 64)
if err != nil {
return fmt.Errorf("parse pending nonce :%w", err)
return nil, fmt.Errorf("parse pending nonce :%w", err)
}
tx := &pb.Transaction{
@ -137,25 +159,23 @@ func sendTransaction(ctx *cli.Context) error {
}
if err := tx.Sign(key.PrivKey); err != nil {
return err
return nil, err
}
reqData, err := json.Marshal(tx)
if err != nil {
return err
return nil, err
}
url, err := getURL(ctx, "transaction")
if err != nil {
return err
return nil, err
}
resp, err := httpPost(ctx, url, reqData)
if err != nil {
return err
return nil, err
}
fmt.Println(string(resp))
return nil
return resp, nil
}