forked from TrueCloudLab/neoneo-go
f011b3c3dd
Notice that int64 types are used for gas per block or registration price because the price has to fit into the system fee limitation and gas per block value can't be more than 10 GAS. We use int64 for votes as well in other types since NEO is limited to 100M.
308 lines
8.2 KiB
Go
308 lines
8.2 KiB
Go
package query
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"sort"
|
|
"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"
|
|
)
|
|
|
|
// NewCommands returns 'query' command.
|
|
func NewCommands() []cli.Command {
|
|
queryTxFlags := append([]cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "verbose, 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 <hash> -r endpoint [-s timeout] [-v]",
|
|
Action: queryTx,
|
|
Flags: queryTxFlags,
|
|
},
|
|
{
|
|
Name: "voter",
|
|
Usage: "Print NEO holder account state",
|
|
UsageText: "neo-go query voter <address> -r endpoint [-s timeout]",
|
|
Action: queryVoter,
|
|
Flags: options.RPC,
|
|
},
|
|
},
|
|
}}
|
|
}
|
|
|
|
func queryTx(ctx *cli.Context) error {
|
|
args := ctx.Args()
|
|
if len(args) == 0 {
|
|
return cli.NewExitError("Transaction hash is missing", 1)
|
|
} else if len(args) > 1 {
|
|
return cli.NewExitError("only one transaction hash is accepted", 1)
|
|
}
|
|
|
|
txHash, err := util.Uint256DecodeStringLE(strings.TrimPrefix(args[0], "0x"))
|
|
if err != nil {
|
|
return cli.NewExitError(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.NewExitError(err, 1)
|
|
}
|
|
|
|
txOut, err := c.GetRawTransactionVerbose(txHash)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
var res *result.ApplicationLog
|
|
if !txOut.Blockhash.Equals(util.Uint256{}) {
|
|
res, err = c.GetApplicationLog(txHash, nil)
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
}
|
|
|
|
DumpApplicationLog(ctx, res, &txOut.Transaction, &txOut.TransactionMetadata, ctx.Bool("verbose"))
|
|
return nil
|
|
}
|
|
|
|
func DumpApplicationLog(
|
|
ctx *cli.Context,
|
|
res *result.ApplicationLog,
|
|
tx *transaction.Transaction,
|
|
txMeta *result.TransactionMetadata,
|
|
verbose bool) {
|
|
buf := bytes.NewBuffer(nil)
|
|
|
|
// Ignore the errors below because `Write` to buffer doesn't return error.
|
|
tw := tabwriter.NewWriter(buf, 0, 4, 4, '\t', 0)
|
|
_, _ = tw.Write([]byte("Hash:\t" + tx.Hash().StringLE() + "\n"))
|
|
_, _ = tw.Write([]byte(fmt.Sprintf("OnChain:\t%t\n", res != nil)))
|
|
if res == nil {
|
|
_, _ = tw.Write([]byte("ValidUntil:\t" + strconv.FormatUint(uint64(tx.ValidUntilBlock), 10) + "\n"))
|
|
} else {
|
|
if txMeta != nil {
|
|
_, _ = tw.Write([]byte("BlockHash:\t" + txMeta.Blockhash.StringLE() + "\n"))
|
|
}
|
|
if len(res.Executions) != 1 {
|
|
_, _ = tw.Write([]byte("Success:\tunknown (no execution data)\n"))
|
|
} else {
|
|
_, _ = tw.Write([]byte(fmt.Sprintf("Success:\t%t\n", res.Executions[0].VMState == vmstate.Halt)))
|
|
}
|
|
}
|
|
if verbose {
|
|
for _, sig := range tx.Signers {
|
|
_, _ = tw.Write([]byte(fmt.Sprintf("Signer:\t%s (%s)",
|
|
address.Uint160ToString(sig.Account),
|
|
sig.Scopes) + "\n"))
|
|
}
|
|
_, _ = tw.Write([]byte("SystemFee:\t" + fixedn.Fixed8(tx.SystemFee).String() + " GAS\n"))
|
|
_, _ = tw.Write([]byte("NetworkFee:\t" + fixedn.Fixed8(tx.NetworkFee).String() + " GAS\n"))
|
|
_, _ = tw.Write([]byte("Script:\t" + base64.StdEncoding.EncodeToString(tx.Script) + "\n"))
|
|
v := vm.New()
|
|
v.Load(tx.Script)
|
|
v.PrintOps(tw)
|
|
if res != nil {
|
|
for _, e := range res.Executions {
|
|
if e.VMState != vmstate.Halt {
|
|
_, _ = tw.Write([]byte("Exception:\t" + e.FaultException + "\n"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
_ = tw.Flush()
|
|
fmt.Fprint(ctx.App.Writer, buf.String())
|
|
}
|
|
|
|
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.NewExitError(err, 1)
|
|
}
|
|
|
|
vals, err := c.GetCandidates()
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
comm, err := c.GetCommittee()
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
sort.Slice(vals, func(i, j int) bool {
|
|
if vals[i].Active != vals[j].Active {
|
|
return vals[i].Active
|
|
}
|
|
if vals[i].Votes != vals[j].Votes {
|
|
return vals[i].Votes > vals[j].Votes
|
|
}
|
|
return vals[i].PublicKey.Cmp(&vals[j].PublicKey) == -1
|
|
})
|
|
buf := bytes.NewBuffer(nil)
|
|
tw := tabwriter.NewWriter(buf, 0, 2, 2, ' ', 0)
|
|
_, _ = tw.Write([]byte("Key\tVotes\tCommittee\tConsensus\n"))
|
|
for _, val := range vals {
|
|
_, _ = tw.Write([]byte(fmt.Sprintf("%s\t%d\t%t\t%t\n", hex.EncodeToString(val.PublicKey.Bytes()), val.Votes, comm.Contains(&val.PublicKey), val.Active)))
|
|
}
|
|
_ = tw.Flush()
|
|
fmt.Fprint(ctx.App.Writer, buf.String())
|
|
return nil
|
|
}
|
|
|
|
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.NewExitError(err, 1)
|
|
}
|
|
|
|
comm, err := c.GetCommittee()
|
|
if err != nil {
|
|
return cli.NewExitError(err, 1)
|
|
}
|
|
|
|
for _, k := range comm {
|
|
fmt.Fprintln(ctx.App.Writer, hex.EncodeToString(k.Bytes()))
|
|
}
|
|
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.NewExitError(err, 1)
|
|
}
|
|
|
|
blockCount, err := c.GetBlockCount()
|
|
if err != nil {
|
|
return cli.NewExitError(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()
|
|
if len(args) == 0 {
|
|
return cli.NewExitError("No address specified", 1)
|
|
} else if len(args) > 1 {
|
|
return cli.NewExitError("this command only accepts one address", 1)
|
|
}
|
|
|
|
addr, err := flags.ParseAddress(args[0])
|
|
if err != nil {
|
|
return cli.NewExitError(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.NewExitError(err, 1)
|
|
}
|
|
if st == nil {
|
|
st = new(state.NEOBalance)
|
|
}
|
|
dec, err := neoToken.Decimals()
|
|
if err != nil {
|
|
return cli.NewExitError(fmt.Errorf("failed to get decimals: %w", err), 1)
|
|
}
|
|
voted := "null"
|
|
if st.VoteTo != nil {
|
|
voted = fmt.Sprintf("%s (%s)", hex.EncodeToString(st.VoteTo.Bytes()), 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
|
|
}
|