forked from TrueCloudLab/neoneo-go
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 <evgeniy@nspcc.ru>
This commit is contained in:
parent
35c2c3ae8e
commit
7d2d9e96ef
5 changed files with 255 additions and 6 deletions
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
110
cli/query/query.go
Normal 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
131
cli/query_test.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in a new issue