cli: add historic abilities for all invoking commands

Notice that we can't do this for balance commands (unless we change the
interface in some manner) because they use getnepXXbalances.

Fixes #2620.
This commit is contained in:
Roman Khimov 2022-09-08 19:05:32 +03:00
parent 4403a95ae6
commit d2d190913b
5 changed files with 194 additions and 62 deletions

View file

@ -602,6 +602,39 @@ func TestContract_TestInvokeScript(t *testing.T) {
"--rpc-endpoint", "http://123456789",
"--in", goodNef)
})
t.Run("good", func(t *testing.T) {
e.Run(t, "neo-go", "contract", "testinvokescript",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--in", goodNef)
})
t.Run("good with hashed signer", func(t *testing.T) {
e.Run(t, "neo-go", "contract", "testinvokescript",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--in", goodNef, "--", util.Uint160{1, 2, 3}.StringLE())
})
t.Run("good with addressed signer", func(t *testing.T) {
e.Run(t, "neo-go", "contract", "testinvokescript",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--in", goodNef, "--", address.Uint160ToString(util.Uint160{1, 2, 3}))
})
t.Run("historic, invalid", func(t *testing.T) {
e.RunWithError(t, "neo-go", "contract", "testinvokescript",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--historic", "bad",
"--in", goodNef)
})
t.Run("historic, index", func(t *testing.T) {
e.Run(t, "neo-go", "contract", "testinvokescript",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--historic", "0",
"--in", goodNef)
})
t.Run("historic, hash", func(t *testing.T) {
e.Run(t, "neo-go", "contract", "testinvokescript",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--historic", e.Chain.GetHeaderHash(0).StringLE(),
"--in", goodNef)
})
}
func TestComlileAndInvokeFunction(t *testing.T) {
@ -618,16 +651,6 @@ func TestComlileAndInvokeFunction(t *testing.T) {
"--config", "testdata/deploy/neo-go.yml",
"--out", nefName, "--manifest", manifestName)
// Check that it is possible to invoke before deploy.
// This doesn't make much sense, because every method has an offset
// which is contained in the manifest. This should be either removed or refactored.
e.Run(t, "neo-go", "contract", "testinvokescript",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--in", nefName, "--", util.Uint160{1, 2, 3}.StringLE())
e.Run(t, "neo-go", "contract", "testinvokescript",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--in", nefName, "--", address.Uint160ToString(util.Uint160{1, 2, 3}))
tmp := t.TempDir()
configPath := filepath.Join(tmp, "config.yaml")
cfg := config.Wallet{
@ -689,11 +712,14 @@ func TestComlileAndInvokeFunction(t *testing.T) {
e.Run(t, cmd...)
checkGetValueOut := func(str string) {
res := new(result.Invoke)
require.NoError(t, json.Unmarshal(e.Out.Bytes(), res))
require.Equal(t, vmstate.Halt.String(), res.State, res.FaultException)
require.Len(t, res.Stack, 1)
require.Equal(t, []byte("on create|sub create"), res.Stack[0].Value())
require.Equal(t, []byte(str), res.Stack[0].Value())
}
checkGetValueOut("on create|sub create")
// deploy verification contract
hVerify := deployVerifyContract(t, e)
@ -866,6 +892,12 @@ func TestComlileAndInvokeFunction(t *testing.T) {
})
})
var (
hashBeforeUpdate util.Uint256
indexBeforeUpdate uint32
indexAfterUpdate uint32
stateBeforeUpdate util.Uint256
)
t.Run("Update", func(t *testing.T) {
nefName := filepath.Join(tmpDir, "updated.nef")
manifestName := filepath.Join(tmpDir, "updated.manifest.json")
@ -884,6 +916,11 @@ func TestComlileAndInvokeFunction(t *testing.T) {
rawManifest, err := os.ReadFile(manifestName)
require.NoError(t, err)
indexBeforeUpdate = e.Chain.BlockHeight()
hashBeforeUpdate = e.Chain.CurrentHeaderHash()
mptBeforeUpdate, err := e.Chain.GetStateRoot(indexBeforeUpdate)
require.NoError(t, err)
stateBeforeUpdate = mptBeforeUpdate.Root
e.In.WriteString("one\r")
e.Run(t, "neo-go", "contract", "invokefunction",
"--rpc-endpoint", "http://"+e.RPC.Addr,
@ -895,16 +932,40 @@ func TestComlileAndInvokeFunction(t *testing.T) {
)
e.checkTxPersisted(t, "Sent invocation transaction ")
indexAfterUpdate = e.Chain.BlockHeight()
e.In.WriteString("one\r")
e.Run(t, "neo-go", "contract", "testinvokefunction",
"--rpc-endpoint", "http://"+e.RPC.Addr,
h.StringLE(), "getValue")
res := new(result.Invoke)
require.NoError(t, json.Unmarshal(e.Out.Bytes(), res))
require.Equal(t, vmstate.Halt.String(), res.State)
require.Len(t, res.Stack, 1)
require.Equal(t, []byte("on update|sub update"), res.Stack[0].Value())
checkGetValueOut("on update|sub update")
})
t.Run("historic", func(t *testing.T) {
t.Run("bad ref", func(t *testing.T) {
e.RunWithError(t, "neo-go", "contract", "testinvokefunction",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--historic", "bad",
h.StringLE(), "getValue")
})
for name, ref := range map[string]string{
"by index": strconv.FormatUint(uint64(indexBeforeUpdate), 10),
"by block hash": hashBeforeUpdate.StringLE(),
"by state hash": stateBeforeUpdate.StringLE(),
} {
t.Run(name, func(t *testing.T) {
e.Run(t, "neo-go", "contract", "testinvokefunction",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--historic", ref,
h.StringLE(), "getValue")
})
checkGetValueOut("on create|sub create")
}
t.Run("updated historic", func(t *testing.T) {
e.Run(t, "neo-go", "contract", "testinvokefunction",
"--rpc-endpoint", "http://"+e.RPC.Addr,
"--historic", strconv.FormatUint(uint64(indexAfterUpdate), 10),
h.StringLE(), "getValue")
checkGetValueOut("on update|sub update")
})
})
}

View file

@ -161,6 +161,7 @@ func TestNEP11_ND_OwnerOf_BalanceOf_Transfer(t *testing.T) {
}
tokenID := mint(t)
var hashBeforeTransfer = e.Chain.CurrentHeaderHash()
// check the balance
cmdCheckBalance := []string{"neo-go", "wallet", "nep11", "balance",
@ -358,6 +359,24 @@ func TestNEP11_ND_OwnerOf_BalanceOf_Transfer(t *testing.T) {
// check balance after transfer
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, nftOwnerAddr)
// historic calls still remember the good old days.
cmdOwnerOf = append(cmdOwnerOf, "--historic", hashBeforeTransfer.StringLE())
e.Run(t, cmdOwnerOf...)
e.checkNextLine(t, nftOwnerAddr)
cmdTokensOf = append(cmdTokensOf, "--historic", hashBeforeTransfer.StringLE())
e.Run(t, cmdTokensOf...)
require.Equal(t, hex.EncodeToString(tokenID), e.getNextLine(t))
cmdTokens = append(cmdTokens, "--historic", hashBeforeTransfer.StringLE())
e.Run(t, cmdTokens...)
require.Equal(t, hex.EncodeToString(tokenID), e.getNextLine(t))
// this one is not affected by transfer, but anyway
cmdProperties = append(cmdProperties, "--historic", hashBeforeTransfer.StringLE())
e.Run(t, cmdProperties...)
require.Equal(t, fmt.Sprintf(`{"name":"HASHY %s"}`, base64.StdEncoding.EncodeToString(tokenID)), e.getNextLine(t))
}
func TestNEP11_D_OwnerOf_BalanceOf_Transfer(t *testing.T) {

View file

@ -6,10 +6,14 @@ package options
import (
"context"
"errors"
"strconv"
"time"
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/urfave/cli"
)
@ -42,7 +46,14 @@ var RPC = []cli.Flag{
},
}
// Historic is a flag for commands that can perform historic invocations.
var Historic = cli.StringFlag{
Name: "historic",
Usage: "Use historic state (height, block hash or state root hash)",
}
var errNoEndpoint = errors.New("no RPC endpoint specified, use option '--" + RPCEndpointFlag + "' or '-r'")
var errInvalidHistoric = errors.New("invalid 'historic' parameter, neither a block number, nor a block/state hash")
// GetNetwork examines Context's flags and returns the appropriate network. It
// defaults to PrivNet if no flags are given.
@ -85,3 +96,35 @@ func GetRPCClient(gctx context.Context, ctx *cli.Context) (*rpcclient.Client, cl
}
return c, nil
}
// GetInvoker returns an invoker using the given RPC client, context and signers.
// It parses "--historic" parameter to adjust it.
func GetInvoker(c *rpcclient.Client, ctx *cli.Context, signers []transaction.Signer) (*invoker.Invoker, cli.ExitCoder) {
historic := ctx.String("historic")
if historic == "" {
return invoker.New(c, signers), nil
}
if index, err := strconv.ParseUint(historic, 10, 32); err == nil {
return invoker.NewHistoricAtHeight(uint32(index), c, signers), nil
}
if u256, err := util.Uint256DecodeStringLE(historic); err == nil {
// Might as well be a block hash, but it makes no practical difference.
return invoker.NewHistoricWithState(u256, c, signers), nil
}
return nil, cli.NewExitError(errInvalidHistoric, 1)
}
// GetRPCWithInvoker combines GetRPCClient with GetInvoker for cases where it's
// appropriate to do so.
func GetRPCWithInvoker(gctx context.Context, ctx *cli.Context, signers []transaction.Signer) (*rpcclient.Client, *invoker.Invoker, cli.ExitCoder) {
c, err := GetRPCClient(gctx, ctx)
if err != nil {
return nil, nil, err
}
inv, err := GetInvoker(c, ctx, signers)
if err != nil {
c.Close()
return nil, nil, err
}
return c, inv, err
}

View file

@ -23,6 +23,7 @@ import (
"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/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
@ -110,8 +111,11 @@ func NewCommands() []cli.Command {
Name: "in, i",
Usage: "Input location of the .nef file that needs to be invoked",
},
options.Historic,
}
testInvokeScriptFlags = append(testInvokeScriptFlags, options.RPC...)
testInvokeFunctionFlags := []cli.Flag{options.Historic}
testInvokeFunctionFlags = append(testInvokeFunctionFlags, options.RPC...)
invokeFunctionFlags := []cli.Flag{
walletFlag,
walletConfigFlag,
@ -213,7 +217,7 @@ func NewCommands() []cli.Command {
{
Name: "testinvokefunction",
Usage: "invoke deployed contract on the blockchain (test mode)",
UsageText: "neo-go contract testinvokefunction -r endpoint scripthash [method] [arguments...] [--] [signers...]",
UsageText: "neo-go contract testinvokefunction -r endpoint [--historic index/hash] scripthash [method] [arguments...] [--] [signers...]",
Description: `Executes given (as a script hash) deployed script with the given method,
arguments and signers (sender is not included by default). If no method is given
"" is passed to the script, if no arguments are given, an empty array is
@ -328,12 +332,12 @@ func NewCommands() []cli.Command {
`CustomContracts:1011120009070e030d0f0e020d0c06050e030c02:0x1211100009070e030d0f0e020d0c06050e030c02'
`,
Action: testInvokeFunction,
Flags: options.RPC,
Flags: testInvokeFunctionFlags,
},
{
Name: "testinvokescript",
Usage: "Invoke compiled AVM code in NEF format on the blockchain (test mode, not creating a transaction for it)",
UsageText: "neo-go contract testinvokescript -r endpoint -i input.nef [signers...]",
UsageText: "neo-go contract testinvokescript -r endpoint -i input.nef [--historic index/hash] [signers...]",
Description: `Executes given compiled AVM instructions in NEF format with the given set of
signers not included sender by default. See testinvokefunction documentation
for the details about parameters.
@ -608,8 +612,9 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
err error
exitErr *cli.ExitError
operation string
params = make([]smartcontract.Parameter, 0)
params []interface{}
paramsStart = 1
scParams []smartcontract.Parameter
cosigners []transaction.Signer
cosignersOffset = 0
)
@ -629,10 +634,14 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
paramsStart++
if len(args) > paramsStart {
cosignersOffset, params, err = cmdargs.ParseParams(args[paramsStart:], true)
cosignersOffset, scParams, err = cmdargs.ParseParams(args[paramsStart:], true)
if err != nil {
return cli.NewExitError(err, 1)
}
params = make([]interface{}, len(scParams))
for i := range scParams {
params[i] = scParams[i]
}
}
cosignersStart := paramsStart + cosignersOffset
@ -656,13 +665,14 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
return invokeWithArgs(ctx, acc, w, script, operation, params, cosigners)
}
func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, script util.Uint160, operation string, params []smartcontract.Parameter, cosigners []transaction.Signer) error {
func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, script util.Uint160, operation string, params []interface{}, cosigners []transaction.Signer) error {
var (
err error
gas, sysgas fixedn.Fixed8
signersAccounts []actor.SignerAccount
resp *result.Invoke
signAndPush = acc != nil
inv *invoker.Invoker
act *actor.Actor
)
if signAndPush {
@ -685,12 +695,15 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet,
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to create RPC actor: %w", err), 1)
}
inv = &act.Invoker
} else {
inv, err = options.GetInvoker(c, ctx, cosigners)
if err != nil {
return err
}
}
out := ctx.String("out")
// It's a bit easier to keep this as is (not using invoker.Invoker)
// during transition period. Mostly because of the need to convert params
// to []interface{}.
resp, err = c.InvokeFunction(script, operation, params, cosigners)
resp, err = inv.Call(script, operation, params...)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -781,12 +794,12 @@ func testInvokeScript(ctx *cli.Context) error {
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
_, inv, err := options.GetRPCWithInvoker(gctx, ctx, signers)
if err != nil {
return err
}
resp, err := c.InvokeScript(nefFile.Script, signers)
resp, err := inv.Run(nefFile.Script)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -922,16 +935,8 @@ func contractDeploy(ctx *cli.Context) error {
return cli.NewExitError(fmt.Errorf("failed to read manifest file: %w", err), 1)
}
appCallParams := []smartcontract.Parameter{
{
Type: smartcontract.ByteArrayType,
Value: f,
},
{
Type: smartcontract.ByteArrayType,
Value: manifestBytes,
},
}
var appCallParams = []interface{}{f, manifestBytes}
signOffset, data, err := cmdargs.ParseParams(ctx.Args(), true)
if err != nil {
return cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1)

View file

@ -13,7 +13,6 @@ import (
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -116,50 +115,55 @@ func newNEP11Commands() []cli.Command {
{
Name: "properties",
Usage: "print properties of NEP-11 token",
UsageText: "properties --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
UsageText: "properties --rpc-endpoint <node> [--timeout <time>] --token <hash> --id <token-id> [--historic <block/hash>]",
Action: printNEP11Properties,
Flags: append([]cli.Flag{
tokenAddressFlag,
tokenID,
options.Historic,
}, options.RPC...),
},
{
Name: "ownerOf",
Usage: "print owner of non-divisible NEP-11 token with the specified ID",
UsageText: "ownerOf --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
UsageText: "ownerOf --rpc-endpoint <node> [--timeout <time>] --token <hash> --id <token-id> [--historic <block/hash>]",
Action: printNEP11NDOwner,
Flags: append([]cli.Flag{
tokenAddressFlag,
tokenID,
options.Historic,
}, options.RPC...),
},
{
Name: "ownerOfD",
Usage: "print set of owners of divisible NEP-11 token with the specified ID (" + maxIters + " will be printed at max)",
UsageText: "ownerOfD --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
UsageText: "ownerOfD --rpc-endpoint <node> [--timeout <time>] --token <hash> --id <token-id> [--historic <block/hash>]",
Action: printNEP11DOwner,
Flags: append([]cli.Flag{
tokenAddressFlag,
tokenID,
options.Historic,
}, options.RPC...),
},
{
Name: "tokensOf",
Usage: "print list of tokens IDs for the specified NFT owner (" + maxIters + " will be printed at max)",
UsageText: "tokensOf --rpc-endpoint <node> --timeout <time> --token <hash> --address <addr>",
UsageText: "tokensOf --rpc-endpoint <node> [--timeout <time>] --token <hash> --address <addr> [--historic <block/hash>]",
Action: printNEP11TokensOf,
Flags: append([]cli.Flag{
tokenAddressFlag,
ownerAddressFlag,
options.Historic,
}, options.RPC...),
},
{
Name: "tokens",
Usage: "print list of tokens IDs minted by the specified NFT (optional method; " + maxIters + " will be printed at max)",
UsageText: "tokens --rpc-endpoint <node> --timeout <time> --token <hash>",
UsageText: "tokens --rpc-endpoint <node> [--timeout <time>] --token <hash> [--historic <block/hash>]",
Action: printNEP11Tokens,
Flags: append([]cli.Flag{
tokenAddressFlag,
options.Historic,
}, options.RPC...),
},
}
@ -256,13 +260,13 @@ func printNEP11Owner(ctx *cli.Context, divisible bool) error {
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
_, inv, err := options.GetRPCWithInvoker(gctx, ctx, nil)
if err != nil {
return cli.NewExitError(err, 1)
return err
}
if divisible {
n11 := nep11.NewDivisibleReader(invoker.New(c, nil), tokenHash.Uint160())
n11 := nep11.NewDivisibleReader(inv, tokenHash.Uint160())
result, err := n11.OwnerOfExpanded(tokenIDBytes, config.DefaultMaxIteratorResultItems)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 divisible `ownerOf` method: %s", err.Error()), 1)
@ -271,7 +275,7 @@ func printNEP11Owner(ctx *cli.Context, divisible bool) error {
fmt.Fprintln(ctx.App.Writer, address.Uint160ToString(h))
}
} else {
n11 := nep11.NewNonDivisibleReader(invoker.New(c, nil), tokenHash.Uint160())
n11 := nep11.NewNonDivisibleReader(inv, tokenHash.Uint160())
result, err := n11.OwnerOf(tokenIDBytes)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 non-divisible `ownerOf` method: %s", err.Error()), 1)
@ -297,12 +301,12 @@ func printNEP11TokensOf(ctx *cli.Context) error {
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
_, inv, err := options.GetRPCWithInvoker(gctx, ctx, nil)
if err != nil {
return cli.NewExitError(err, 1)
return err
}
n11 := nep11.NewBaseReader(invoker.New(c, nil), tokenHash.Uint160())
n11 := nep11.NewBaseReader(inv, tokenHash.Uint160())
result, err := n11.TokensOfExpanded(acc.Uint160(), config.DefaultMaxIteratorResultItems)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `tokensOf` method: %s", err.Error()), 1)
@ -327,12 +331,12 @@ func printNEP11Tokens(ctx *cli.Context) error {
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
_, inv, err := options.GetRPCWithInvoker(gctx, ctx, nil)
if err != nil {
return cli.NewExitError(err, 1)
return err
}
n11 := nep11.NewBaseReader(invoker.New(c, nil), tokenHash.Uint160())
n11 := nep11.NewBaseReader(inv, tokenHash.Uint160())
result, err := n11.TokensExpanded(config.DefaultMaxIteratorResultItems)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call optional NEP-11 `tokens` method: %s", err.Error()), 1)
@ -366,12 +370,12 @@ func printNEP11Properties(ctx *cli.Context) error {
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
_, inv, err := options.GetRPCWithInvoker(gctx, ctx, nil)
if err != nil {
return cli.NewExitError(err, 1)
return err
}
n11 := nep11.NewBaseReader(invoker.New(c, nil), tokenHash.Uint160())
n11 := nep11.NewBaseReader(inv, tokenHash.Uint160())
result, err := n11.Properties(tokenIDBytes)
if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `properties` method: %s", err.Error()), 1)