diff --git a/cli/contract_test.go b/cli/contract_test.go index d0cc32596..55e6bfa6d 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -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...) - 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()) + 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(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") + }) }) } diff --git a/cli/nep11_test.go b/cli/nep11_test.go index 771af8446..a652ec094 100644 --- a/cli/nep11_test.go +++ b/cli/nep11_test.go @@ -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) { diff --git a/cli/options/options.go b/cli/options/options.go index cc73d3a97..cb23cb72c 100644 --- a/cli/options/options.go +++ b/cli/options/options.go @@ -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 +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 009cf94e0..ef2159dd3 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -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 @@ -653,18 +662,17 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { defer w.Close() } - _, err = invokeWithArgs(ctx, acc, w, script, operation, params, cosigners) - return err + 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) (util.Uint160, 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 - sender util.Uint160 signAndPush = acc != nil + inv *invoker.Invoker act *actor.Actor ) if signAndPush { @@ -672,35 +680,37 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, sysgas = flags.Fixed8FromContext(ctx, "sysgas") signersAccounts, err = cmdargs.GetSignersAccounts(acc, wall, cosigners, transaction.None) if err != nil { - return sender, cli.NewExitError(fmt.Errorf("invalid signers: %w", err), 1) + return cli.NewExitError(fmt.Errorf("invalid signers: %w", err), 1) } - sender = signersAccounts[0].Signer.Account } gctx, cancel := options.GetTimeoutContext(ctx) defer cancel() c, err := options.GetRPCClient(gctx, ctx) if err != nil { - return sender, err + return err } if signAndPush { act, err = actor.New(c, signersAccounts) if err != nil { - return sender, cli.NewExitError(fmt.Errorf("failed to create RPC actor: %w", err), 1) + 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 sender, cli.NewExitError(err, 1) + return cli.NewExitError(err, 1) } if resp.State != "HALT" { errText := fmt.Sprintf("Warning: %s VM state returned from the RPC node: %s", resp.State, resp.FaultException) if !signAndPush { - return sender, cli.NewExitError(errText, 1) + return cli.NewExitError(errText, 1) } action := "send" @@ -710,25 +720,25 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, process = "Saving" } if !ctx.Bool("force") { - return sender, cli.NewExitError(errText+".\nUse --force flag to "+action+" the transaction anyway.", 1) + return cli.NewExitError(errText+".\nUse --force flag to "+action+" the transaction anyway.", 1) } fmt.Fprintln(ctx.App.Writer, errText+".\n"+process+" transaction...") } if !signAndPush { b, err := json.MarshalIndent(resp, "", " ") if err != nil { - return sender, cli.NewExitError(err, 1) + return cli.NewExitError(err, 1) } fmt.Fprintln(ctx.App.Writer, string(b)) } else { if len(resp.Script) == 0 { - return sender, cli.NewExitError(errors.New("no script returned from the RPC node"), 1) + return cli.NewExitError(errors.New("no script returned from the RPC node"), 1) } ver := act.GetVersion() tx, err := act.MakeUnsignedUncheckedRun(resp.Script, resp.GasConsumed+int64(sysgas), nil) if err != nil { - return sender, cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1) + return cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1) } tx.NetworkFee += int64(gas) if out != "" { @@ -736,7 +746,7 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, tx.ValidUntilBlock += (ver.Protocol.MaxValidUntilBlockIncrement - uint32(ver.Protocol.ValidatorsCount)) - 2 m := act.GetNetwork() if err := paramcontext.InitAndSave(m, tx, acc, out); err != nil { - return sender, cli.NewExitError(err, 1) + return cli.NewExitError(err, 1) } fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) } else { @@ -744,7 +754,7 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, promptTime := time.Now() err := input.ConfirmTx(ctx.App.Writer, tx) if err != nil { - return sender, cli.NewExitError(err, 1) + return cli.NewExitError(err, 1) } waitTime := time.Since(promptTime) // Compensate for confirmation waiting. @@ -752,13 +762,13 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, } txHash, _, err := act.SignAndSend(tx) if err != nil { - return sender, cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1) + return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1) } fmt.Fprintf(ctx.App.Writer, "Sent invocation transaction %s\n", txHash.StringLE()) } } - return sender, nil + return nil } func testInvokeScript(ctx *cli.Context) error { @@ -784,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) } @@ -925,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) @@ -951,6 +953,7 @@ func contractDeploy(ctx *cli.Context) error { return cli.NewExitError(fmt.Errorf("can't get sender address: %w", err), 1) } defer w.Close() + sender := acc.ScriptHash() cosigners, sgnErr := cmdargs.GetSignersFromContext(ctx, signOffset) if sgnErr != nil { @@ -962,7 +965,7 @@ func contractDeploy(ctx *cli.Context) error { }} } - sender, extErr := invokeWithArgs(ctx, acc, w, management.Hash, "deploy", appCallParams, cosigners) + extErr := invokeWithArgs(ctx, acc, w, management.Hash, "deploy", appCallParams, cosigners) if extErr != nil { return extErr } diff --git a/cli/wallet/nep11.go b/cli/wallet/nep11.go index 1d7034957..5fb8c950e 100644 --- a/cli/wallet/nep11.go +++ b/cli/wallet/nep11.go @@ -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 --timeout