Merge pull request #2070 from nspcc-dev/cli-query

cli: add `query tx` command, close #2069
This commit is contained in:
Roman Khimov 2021-07-20 18:14:11 +03:00 committed by GitHub
commit bf176fb637
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 269 additions and 8 deletions

View file

@ -142,7 +142,7 @@ func TestContractInitAndCompile(t *testing.T) {
// Checks that error is returned if GAS available for test-invoke exceeds // Checks that error is returned if GAS available for test-invoke exceeds
// GAS needed to be consumed. // GAS needed to be consumed.
func TestDeployBigContract(t *testing.T) { func TestDeployBigContract(t *testing.T) {
e := newExecutorWithConfig(t, true, func(c *config.Config) { e := newExecutorWithConfig(t, true, true, func(c *config.Config) {
c.ApplicationConfiguration.RPC.MaxGasInvoke = fixedn.Fixed8(1) c.ApplicationConfiguration.RPC.MaxGasInvoke = fixedn.Fixed8(1)
}) })

View file

@ -61,7 +61,7 @@ type executor struct {
In *bytes.Buffer In *bytes.Buffer
} }
func newTestChain(t *testing.T, f func(*config.Config)) (*core.Blockchain, *server.Server, *network.Server) { func newTestChain(t *testing.T, f func(*config.Config), run bool) (*core.Blockchain, *server.Server, *network.Server) {
configPath := "../config/protocol.unit_testnet.single.yml" configPath := "../config/protocol.unit_testnet.single.yml"
cfg, err := config.LoadFile(configPath) cfg, err := config.LoadFile(configPath)
require.NoError(t, err, "could not load config") require.NoError(t, err, "could not load config")
@ -74,7 +74,9 @@ func newTestChain(t *testing.T, f func(*config.Config)) (*core.Blockchain, *serv
chain, err := core.NewBlockchain(memoryStore, cfg.ProtocolConfiguration, logger) chain, err := core.NewBlockchain(memoryStore, cfg.ProtocolConfiguration, logger)
require.NoError(t, err, "could not create chain") require.NoError(t, err, "could not create chain")
go chain.Run() if run {
go chain.Run()
}
serverConfig := network.NewServerConfig(cfg) serverConfig := network.NewServerConfig(cfg)
netSrv, err := network.NewServer(serverConfig, chain, zap.NewNop()) netSrv, err := network.NewServer(serverConfig, chain, zap.NewNop())
@ -88,10 +90,14 @@ func newTestChain(t *testing.T, f func(*config.Config)) (*core.Blockchain, *serv
} }
func newExecutor(t *testing.T, needChain bool) *executor { func newExecutor(t *testing.T, needChain bool) *executor {
return newExecutorWithConfig(t, needChain, nil) return newExecutorWithConfig(t, needChain, true, nil)
} }
func newExecutorWithConfig(t *testing.T, needChain bool, f func(*config.Config)) *executor { func newExecutorSuspended(t *testing.T) *executor {
return newExecutorWithConfig(t, true, false, nil)
}
func newExecutorWithConfig(t *testing.T, needChain, runChain bool, f func(*config.Config)) *executor {
e := &executor{ e := &executor{
CLI: newApp(), CLI: newApp(),
Out: bytes.NewBuffer(nil), Out: bytes.NewBuffer(nil),
@ -101,7 +107,7 @@ func newExecutorWithConfig(t *testing.T, needChain bool, f func(*config.Config))
e.CLI.Writer = e.Out e.CLI.Writer = e.Out
e.CLI.ErrWriter = e.Err e.CLI.ErrWriter = e.Err
if needChain { if needChain {
e.Chain, e.RPC, e.NetSrv = newTestChain(t, f) e.Chain, e.RPC, e.NetSrv = newTestChain(t, f, runChain)
} }
t.Cleanup(func() { t.Cleanup(func() {
e.Close(t) e.Close(t)

View file

@ -3,6 +3,7 @@ package main
import ( import (
"os" "os"
"github.com/nspcc-dev/neo-go/cli/query"
"github.com/nspcc-dev/neo-go/cli/server" "github.com/nspcc-dev/neo-go/cli/server"
"github.com/nspcc-dev/neo-go/cli/smartcontract" "github.com/nspcc-dev/neo-go/cli/smartcontract"
"github.com/nspcc-dev/neo-go/cli/util" "github.com/nspcc-dev/neo-go/cli/util"
@ -32,5 +33,6 @@ func newApp() *cli.App {
ctl.Commands = append(ctl.Commands, wallet.NewCommands()...) ctl.Commands = append(ctl.Commands, wallet.NewCommands()...)
ctl.Commands = append(ctl.Commands, vm.NewCommands()...) ctl.Commands = append(ctl.Commands, vm.NewCommands()...)
ctl.Commands = append(ctl.Commands, util.NewCommands()...) ctl.Commands = append(ctl.Commands, util.NewCommands()...)
ctl.Commands = append(ctl.Commands, query.NewCommands()...)
return ctl return ctl
} }

110
cli/query/query.go Normal file
View file

@ -0,0 +1,110 @@
package query
import (
"bytes"
"encoding/base64"
"fmt"
"strconv"
"strings"
"text/tabwriter"
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/rpc/response/result"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"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",
Subcommands: []cli.Command{
{
Name: "tx",
Usage: "query tx status",
Action: queryTx,
Flags: queryTxFlags,
},
},
}}
}
func queryTx(ctx *cli.Context) error {
args := ctx.Args()
if len(args) == 0 {
return cli.NewExitError("Transaction hash is missing", 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)
return nil
}
func dumpApplicationLog(ctx *cli.Context, res *result.ApplicationLog, tx *result.TransactionOutputRaw) {
verbose := ctx.Bool("verbose")
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 {
_, _ = tw.Write([]byte("BlockHash:\t" + tx.Blockhash.StringLE() + "\n"))
_, _ = tw.Write([]byte(fmt.Sprintf("Success:\t%t\n", tx.VMState == vm.HaltState.String())))
}
if verbose {
for _, sig := range tx.Signers {
_, _ = tw.Write([]byte(fmt.Sprintf("Signer:\t%s (%s)",
sig.Account.StringLE(),
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"))
if res != nil {
for _, e := range res.Executions {
if e.VMState != vm.HaltState {
_, _ = tw.Write([]byte("Exception:\t" + e.FaultException + "\n"))
}
}
}
}
_ = tw.Flush()
fmt.Fprint(ctx.App.Writer, buf.String())
}

131
cli/query_test.go Normal file
View file

@ -0,0 +1,131 @@
package main
import (
"encoding/base64"
"fmt"
"regexp"
"strconv"
"strings"
"testing"
"time"
"github.com/nspcc-dev/neo-go/internal/random"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require"
)
func TestQueryTx(t *testing.T) {
e := newExecutorSuspended(t)
w, err := wallet.NewWalletFromFile("testdata/testwallet.json")
require.NoError(t, err)
defer w.Close()
transferArgs := []string{
"neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://" + e.RPC.Addr,
"--wallet", validatorWallet,
"--to", w.Accounts[0].Address,
"--token", "NEO",
"--from", validatorAddr,
}
e.In.WriteString("one\r")
e.Run(t, append(transferArgs, "--amount", "1")...)
line := e.getNextLine(t)
txHash, err := util.Uint256DecodeStringLE(line)
require.NoError(t, err)
tx, ok := e.Chain.GetMemPool().TryGetValue(txHash)
require.True(t, ok)
args := []string{"neo-go", "query", "tx", "--rpc-endpoint", "http://" + e.RPC.Addr}
e.Run(t, append(args, txHash.StringLE())...)
e.checkNextLine(t, `Hash:\s+`+txHash.StringLE())
e.checkNextLine(t, `OnChain:\s+false`)
e.checkNextLine(t, `ValidUntil:\s+`+strconv.FormatUint(uint64(tx.ValidUntilBlock), 10))
e.checkEOF(t)
height := e.Chain.BlockHeight()
go e.Chain.Run()
require.Eventually(t, func() bool { return e.Chain.BlockHeight() > height }, time.Second*2, time.Millisecond*50)
e.Run(t, append(args, txHash.StringLE())...)
e.checkNextLine(t, `Hash:\s+`+txHash.StringLE())
e.checkNextLine(t, `OnChain:\s+true`)
_, height, err = e.Chain.GetTransaction(txHash)
require.NoError(t, err)
e.checkNextLine(t, `BlockHash:\s+`+e.Chain.GetHeaderHash(int(height)).StringLE())
e.checkNextLine(t, `Success:\s+true`)
e.checkEOF(t)
t.Run("verbose", func(t *testing.T) {
e.Run(t, append(args, "--verbose", txHash.StringLE())...)
e.compareQueryTxVerbose(t, tx)
t.Run("FAULT", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, "neo-go", "contract", "invokefunction",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--wallet", validatorWallet,
"--address", validatorAddr,
"--force",
random.Uint160().StringLE(),
"randomMethod")
e.checkNextLine(t, `Warning:`)
e.checkNextLine(t, "Sending transaction")
line := strings.TrimPrefix(e.getNextLine(t), "Sent invocation transaction ")
txHash, err := util.Uint256DecodeStringLE(line)
require.NoError(t, err)
height := e.Chain.BlockHeight()
require.Eventually(t, func() bool { return e.Chain.BlockHeight() > height }, time.Second*2, time.Millisecond*50)
tx, _, err := e.Chain.GetTransaction(txHash)
require.NoError(t, err)
e.Run(t, append(args, "--verbose", txHash.StringLE())...)
e.compareQueryTxVerbose(t, tx)
})
})
t.Run("invalid", func(t *testing.T) {
t.Run("missing tx argument", func(t *testing.T) {
e.RunWithError(t, args...)
})
t.Run("invalid hash", func(t *testing.T) {
e.RunWithError(t, append(args, "notahash")...)
})
t.Run("good hash, missing tx", func(t *testing.T) {
e.RunWithError(t, append(args, random.Uint256().StringLE())...)
})
})
}
func (e *executor) compareQueryTxVerbose(t *testing.T, tx *transaction.Transaction) {
e.checkNextLine(t, `Hash:\s+`+tx.Hash().StringLE())
e.checkNextLine(t, `OnChain:\s+true`)
_, height, err := e.Chain.GetTransaction(tx.Hash())
require.NoError(t, err)
e.checkNextLine(t, `BlockHash:\s+`+e.Chain.GetHeaderHash(int(height)).StringLE())
res, _ := e.Chain.GetAppExecResults(tx.Hash(), trigger.Application)
e.checkNextLine(t, fmt.Sprintf(`Success:\s+%t`, res[0].Execution.VMState == vm.HaltState))
for _, s := range tx.Signers {
e.checkNextLine(t, fmt.Sprintf(`Signer:\s+%s\s*\(%s\)`, s.Account.StringLE(), s.Scopes.String()))
}
e.checkNextLine(t, `SystemFee:\s+`+fixedn.Fixed8(tx.SystemFee).String()+" GAS$")
e.checkNextLine(t, `NetworkFee:\s+`+fixedn.Fixed8(tx.NetworkFee).String()+" GAS$")
e.checkNextLine(t, `Script:\s+`+regexp.QuoteMeta(base64.StdEncoding.EncodeToString(tx.Script)))
if res[0].Execution.VMState != vm.HaltState {
e.checkNextLine(t, `Exception:\s+`+regexp.QuoteMeta(res[0].Execution.FaultException))
}
e.checkEOF(t)
}

View file

@ -464,6 +464,18 @@ You can also vote for candidates if you own NEO:
./bin/neo-go wallet candidate vote -a NMe64G6j6nkPZby26JAgpaCNrn1Ee4wW6E -w wallet.json -r http://localhost:20332 -c 03cecd63d7d8120c3b194c3b2880dd4aafe1475c57e40c852872d7305615258140 ./bin/neo-go wallet candidate vote -a NMe64G6j6nkPZby26JAgpaCNrn1Ee4wW6E -w wallet.json -r http://localhost:20332 -c 03cecd63d7d8120c3b194c3b2880dd4aafe1475c57e40c852872d7305615258140
``` ```
### Querying transaction status
`query tx` provides convenient wrapper over RPC calls to query transaction status.
```
./bin/neo-go query tx --rpc-endpoint http://localhost:20332 aaf87628851e0c03ee086ff88596bc24de87082e9e5c73d75bb1c740d1d68088
Hash: aaf87628851e0c03ee086ff88596bc24de87082e9e5c73d75bb1c740d1d68088
OnChain: true
BlockHash: fabcd46e93b8f4e1bc5689e3e0cc59704320494f7a0265b91ae78b4d747ee93b
Success: true
```
`OnChain` is true if transaction was included in block and `Success` is true
if it was executed successfully.
### NEP-17 token functions ### NEP-17 token functions
`wallet nep17` contains a set of commands to use for NEP-17 tokens. `wallet nep17` contains a set of commands to use for NEP-17 tokens.

View file

@ -91,8 +91,8 @@ use.
This command will create and send appropriate transaction to the network and This command will create and send appropriate transaction to the network and
you should then wait for it to settle in a block. If all goes well it'll end you should then wait for it to settle in a block. If all goes well it'll end
with "HALT" state and your registration will be completed. You can make with "HALT" state and your registration will be completed. You can use
`getapplicationlog` RPC requests to see transaction status or `query tx` command to see transaction status or
`getnextblockvalidators` to see if your candidate was added. `getnextblockvalidators` to see if your candidate was added.
### Voting ### Voting