diff --git a/api/gateway/gateway.go b/api/gateway/gateway.go index 2bfc227..17a8ae6 100644 --- a/api/gateway/gateway.go +++ b/api/gateway/gateway.go @@ -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}, ), ) diff --git a/cmd/bitxhub/client/client.go b/cmd/bitxhub/client/client.go index 5d7cc5f..7126f6a 100644 --- a/cmd/bitxhub/client/client.go +++ b/cmd/bitxhub/client/client.go @@ -25,6 +25,7 @@ var clientCMD = cli.Command{ txCMD(), validatorsCMD(), delVPNodeCMD(), + governanceCMD(), }, } diff --git a/cmd/bitxhub/client/governance.go b/cmd/bitxhub/client/governance.go new file mode 100644 index 0000000..9dcac0b --- /dev/null +++ b/cmd/bitxhub/client/governance.go @@ -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 +} diff --git a/cmd/bitxhub/client/receipt.go b/cmd/bitxhub/client/receipt.go index 2802b84..b9eead8 100644 --- a/cmd/bitxhub/client/receipt.go +++ b/cmd/bitxhub/client/receipt.go @@ -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 +} diff --git a/cmd/bitxhub/client/transaction.go b/cmd/bitxhub/client/transaction.go index 84a6041..bb74512 100644 --- a/cmd/bitxhub/client/transaction.go +++ b/cmd/bitxhub/client/transaction.go @@ -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 }