neo-go/cli/query/query.go
Roman Khimov a1a7e3d708 *: use slices package for sorting and searching
Signed-off-by: Roman Khimov <roman@nspcc.ru>
2024-08-27 12:29:44 +03:00

320 lines
8 KiB
Go

package query
import (
"bytes"
"cmp"
"encoding/base64"
"fmt"
"slices"
"strconv"
"strings"
"text/tabwriter"
"github.com/nspcc-dev/neo-go/cli/cmdargs"
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
"github.com/urfave/cli/v2"
)
// NewCommands returns 'query' command.
func NewCommands() []*cli.Command {
queryTxFlags := append([]cli.Flag{
&cli.BoolFlag{
Name: "verbose",
Aliases: []string{"v"},
Usage: "Output full tx info and execution logs",
},
}, options.RPC...)
return []*cli.Command{{
Name: "query",
Usage: "Query data from RPC node",
Subcommands: []*cli.Command{
{
Name: "candidates",
Usage: "Get candidates and votes",
UsageText: "neo-go query candidates -r endpoint [-s timeout]",
Action: queryCandidates,
Flags: options.RPC,
},
{
Name: "committee",
Usage: "Get committee list",
UsageText: "neo-go query committee -r endpoint [-s timeout]",
Action: queryCommittee,
Flags: options.RPC,
},
{
Name: "height",
Usage: "Get node height",
UsageText: "neo-go query height -r endpoint [-s timeout]",
Action: queryHeight,
Flags: options.RPC,
},
{
Name: "tx",
Usage: "Query transaction status",
UsageText: "neo-go query tx -r endpoint [-s timeout] [-v] <hash>",
Action: queryTx,
Flags: queryTxFlags,
},
{
Name: "voter",
Usage: "Print NEO holder account state",
UsageText: "neo-go query voter -r endpoint [-s timeout] <address>",
Action: queryVoter,
Flags: options.RPC,
},
},
}}
}
func queryTx(ctx *cli.Context) error {
args := ctx.Args().Slice()
if len(args) == 0 {
return cli.Exit("transaction hash is missing", 1)
} else if len(args) > 1 {
return cli.Exit("only one transaction hash is accepted", 1)
}
txHash, err := util.Uint256DecodeStringLE(strings.TrimPrefix(args[0], "0x"))
if err != nil {
return cli.Exit(fmt.Sprintf("invalid tx hash: %s", args[0]), 1)
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.Exit(err, 1)
}
txOut, err := c.GetRawTransactionVerbose(txHash)
if err != nil {
return cli.Exit(err, 1)
}
var res *result.ApplicationLog
if !txOut.Blockhash.Equals(util.Uint256{}) {
res, err = c.GetApplicationLog(txHash, nil)
if err != nil {
return cli.Exit(err, 1)
}
}
err = DumpApplicationLog(ctx, res, &txOut.Transaction, &txOut.TransactionMetadata, ctx.Bool("verbose"))
if err != nil {
return cli.Exit(err, 1)
}
return nil
}
func DumpApplicationLog(
ctx *cli.Context,
res *result.ApplicationLog,
tx *transaction.Transaction,
txMeta *result.TransactionMetadata,
verbose bool) error {
var buf []byte
buf = fmt.Appendf(buf, "Hash:\t%s\n", tx.Hash().StringLE())
buf = fmt.Appendf(buf, "OnChain:\t%t\n", res != nil)
if res == nil {
buf = fmt.Appendf(buf, "ValidUntil:\t%s\n", strconv.FormatUint(uint64(tx.ValidUntilBlock), 10))
} else {
if txMeta != nil {
buf = fmt.Appendf(buf, "BlockHash:\t%s\n", txMeta.Blockhash.StringLE())
}
if len(res.Executions) != 1 {
buf = fmt.Appendf(buf, "Success:\tunknown (no execution data)\n")
} else {
buf = fmt.Appendf(buf, "Success:\t%t\n", res.Executions[0].VMState == vmstate.Halt)
}
}
if verbose {
for _, sig := range tx.Signers {
buf = fmt.Appendf(buf, "Signer:\t%s (%s)\n", address.Uint160ToString(sig.Account), sig.Scopes)
}
buf = fmt.Appendf(buf, "SystemFee:\t%s GAS\n", fixedn.Fixed8(tx.SystemFee).String())
buf = fmt.Appendf(buf, "NetworkFee:\t%s GAS\n", fixedn.Fixed8(tx.NetworkFee).String())
buf = fmt.Appendf(buf, "Script:\t%s\n", base64.StdEncoding.EncodeToString(tx.Script))
v := vm.New()
v.Load(tx.Script)
opts := bytes.NewBuffer(nil)
v.PrintOps(opts)
buf = append(buf, opts.Bytes()...)
if res != nil {
for _, e := range res.Executions {
if e.VMState != vmstate.Halt {
buf = fmt.Appendf(buf, "Exception:\t%s\n", e.FaultException)
}
}
}
}
tw := tabwriter.NewWriter(ctx.App.Writer, 0, 4, 4, '\t', 0)
_, err := tw.Write(buf)
if err != nil {
return err
}
return tw.Flush()
}
func queryCandidates(ctx *cli.Context) error {
var err error
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.Exit(err, 1)
}
vals, err := c.GetCandidates()
if err != nil {
return cli.Exit(err, 1)
}
comm, err := c.GetCommittee()
if err != nil {
return cli.Exit(err, 1)
}
slices.SortFunc(vals, func(a, b result.Candidate) int {
if a.Active && !b.Active {
return 1
}
if !a.Active && b.Active {
return -1
}
res := cmp.Compare(a.Votes, b.Votes)
if res != 0 {
return res
}
return a.PublicKey.Cmp(&b.PublicKey)
})
var res []byte
res = fmt.Appendf(res, "Key\tVotes\tCommittee\tConsensus\n")
for _, val := range vals {
res = fmt.Appendf(res, "%s\t%d\t%t\t%t\n", val.PublicKey.StringCompressed(), val.Votes, comm.Contains(&val.PublicKey), val.Active)
}
tw := tabwriter.NewWriter(ctx.App.Writer, 0, 2, 2, ' ', 0)
_, err = tw.Write(res)
if err != nil {
return err
}
return tw.Flush()
}
func queryCommittee(ctx *cli.Context) error {
var err error
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.Exit(err, 1)
}
comm, err := c.GetCommittee()
if err != nil {
return cli.Exit(err, 1)
}
for _, k := range comm {
fmt.Fprintln(ctx.App.Writer, k.StringCompressed())
}
return nil
}
func queryHeight(ctx *cli.Context) error {
var err error
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.Exit(err, 1)
}
blockCount, err := c.GetBlockCount()
if err != nil {
return cli.Exit(err, 1)
}
blockHeight := blockCount - 1 // GetBlockCount returns block count (including 0), not the highest block index.
fmt.Fprintf(ctx.App.Writer, "Latest block: %d\n", blockHeight)
stateHeight, err := c.GetStateHeight()
if err == nil { // We can be talking to a node without getstateheight request support.
fmt.Fprintf(ctx.App.Writer, "Validated state: %d\n", stateHeight.Validated)
}
return nil
}
func queryVoter(ctx *cli.Context) error {
args := ctx.Args().Slice()
if len(args) == 0 {
return cli.Exit("no address specified", 1)
} else if len(args) > 1 {
return cli.Exit("this command only accepts one address", 1)
}
addr, err := flags.ParseAddress(args[0])
if err != nil {
return cli.Exit(fmt.Sprintf("wrong address: %s", args[0]), 1)
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, exitErr := options.GetRPCClient(gctx, ctx)
if exitErr != nil {
return exitErr
}
neoToken := neo.NewReader(invoker.New(c, nil))
st, err := neoToken.GetAccountState(addr)
if err != nil {
return cli.Exit(err, 1)
}
if st == nil {
st = new(state.NEOBalance)
}
dec, err := neoToken.Decimals()
if err != nil {
return cli.Exit(fmt.Errorf("failed to get decimals: %w", err), 1)
}
voted := "null"
if st.VoteTo != nil {
voted = fmt.Sprintf("%s (%s)", st.VoteTo.StringCompressed(), address.Uint160ToString(st.VoteTo.GetScriptHash()))
}
fmt.Fprintf(ctx.App.Writer, "\tVoted: %s\n", voted)
fmt.Fprintf(ctx.App.Writer, "\tAmount : %s\n", fixedn.ToString(&st.Balance, int(dec)))
fmt.Fprintf(ctx.App.Writer, "\tBlock: %d\n", st.BalanceHeight)
return nil
}