From 7d2d9e96ef3ec4c6767a3cc02165f3de9820ebbf Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Fri, 16 Jul 2021 17:47:40 +0300 Subject: [PATCH 1/2] cli: add `query tx` command, fix #2069 Implement a way to check if tx has been persisted on blockchain and to get general info about transaction. Much more convenient than handwritten curl queries. Signed-off-by: Evgeniy Stratonikov --- cli/contract_test.go | 2 +- cli/executor_test.go | 16 ++++-- cli/main.go | 2 + cli/query/query.go | 110 ++++++++++++++++++++++++++++++++++++ cli/query_test.go | 131 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 cli/query/query.go create mode 100644 cli/query_test.go diff --git a/cli/contract_test.go b/cli/contract_test.go index 39ab9b35d..964d782d5 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -142,7 +142,7 @@ func TestContractInitAndCompile(t *testing.T) { // Checks that error is returned if GAS available for test-invoke exceeds // GAS needed to be consumed. 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) }) diff --git a/cli/executor_test.go b/cli/executor_test.go index e178e11c2..835ac77dd 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -61,7 +61,7 @@ type executor struct { 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" cfg, err := config.LoadFile(configPath) 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) require.NoError(t, err, "could not create chain") - go chain.Run() + if run { + go chain.Run() + } serverConfig := network.NewServerConfig(cfg) 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 { - 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{ CLI: newApp(), 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.ErrWriter = e.Err if needChain { - e.Chain, e.RPC, e.NetSrv = newTestChain(t, f) + e.Chain, e.RPC, e.NetSrv = newTestChain(t, f, runChain) } t.Cleanup(func() { e.Close(t) diff --git a/cli/main.go b/cli/main.go index 9d81b36bf..5c35efa42 100644 --- a/cli/main.go +++ b/cli/main.go @@ -3,6 +3,7 @@ package main import ( "os" + "github.com/nspcc-dev/neo-go/cli/query" "github.com/nspcc-dev/neo-go/cli/server" "github.com/nspcc-dev/neo-go/cli/smartcontract" "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, vm.NewCommands()...) ctl.Commands = append(ctl.Commands, util.NewCommands()...) + ctl.Commands = append(ctl.Commands, query.NewCommands()...) return ctl } diff --git a/cli/query/query.go b/cli/query/query.go new file mode 100644 index 000000000..d77850591 --- /dev/null +++ b/cli/query/query.go @@ -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()) +} diff --git a/cli/query_test.go b/cli/query_test.go new file mode 100644 index 000000000..d267b4903 --- /dev/null +++ b/cli/query_test.go @@ -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) +} From 4861569ab69cb25b7ebe66758680f4285285cb55 Mon Sep 17 00:00:00 2001 From: Evgeniy Stratonikov Date: Tue, 20 Jul 2021 13:25:35 +0300 Subject: [PATCH 2/2] docs: add `query tx` info Signed-off-by: Evgeniy Stratonikov --- docs/cli.md | 12 ++++++++++++ docs/consensus.md | 4 ++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/cli.md b/docs/cli.md index c3b182c49..5f0939e8b 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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 ``` +### 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 `wallet nep17` contains a set of commands to use for NEP-17 tokens. diff --git a/docs/consensus.md b/docs/consensus.md index c5231ba91..1ddd12e0b 100644 --- a/docs/consensus.md +++ b/docs/consensus.md @@ -91,8 +91,8 @@ use. 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 -with "HALT" state and your registration will be completed. You can make -`getapplicationlog` RPC requests to see transaction status or +with "HALT" state and your registration will be completed. You can use +`query tx` command to see transaction status or `getnextblockvalidators` to see if your candidate was added. ### Voting