rpc: update CLI and RPC client invoke* calls

part of #1036
This commit is contained in:
Anna Shaleva 2020-06-11 11:45:17 +03:00
parent d5355acfa9
commit 9e7fca013e
6 changed files with 225 additions and 39 deletions

View file

@ -16,6 +16,7 @@ import (
"github.com/go-yaml/yaml"
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/pkg/compiler"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/rpc/client"
@ -68,6 +69,9 @@ import "github.com/nspcc-dev/neo-go/pkg/interop/runtime"
func Main(op string, args []interface{}) {
runtime.Notify("Hello world!")
}`
// cosignersSeparator is a special value which is used to distinguish
// parameters and cosigners for invoke* commands
cosignersSeparator = "--"
)
// NewCommands returns 'contract' command.
@ -135,11 +139,11 @@ func NewCommands() []cli.Command {
{
Name: "invokefunction",
Usage: "invoke deployed contract on the blockchain",
UsageText: "neo-go contract invokefunction -e endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...]",
Description: `Executes given (as a script hash) deployed script with the given method and
and arguments. See testinvokefunction documentation for the details about
parameters. It differs from testinvokefunction in that this command sends an
invocation transaction to the network.
UsageText: "neo-go contract invokefunction -e endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...] [--] [cosigners...]",
Description: `Executes given (as a script hash) deployed script with the given method,
arguments and cosigners. See testinvokefunction documentation for the details
about parameters. It differs from testinvokefunction in that this command
sends an invocation transaction to the network.
`,
Action: invokeFunction,
Flags: []cli.Flag{
@ -152,13 +156,14 @@ func NewCommands() []cli.Command {
{
Name: "testinvokefunction",
Usage: "invoke deployed contract on the blockchain (test mode)",
UsageText: "neo-go contract testinvokefunction -e endpoint scripthash [method] [arguments...]",
Description: `Executes given (as a script hash) deployed script with the given method and
arguments. If no method is given "" is passed to the script, if no arguments
are given, an empty array is passed. All of the given arguments are
encapsulated into array before invoking the script. The script thus should
follow the regular convention of smart contract arguments (method string and
an array of other arguments).
UsageText: "neo-go contract testinvokefunction -e endpoint scripthash [method] [arguments...] [--] [cosigners...]",
Description: `Executes given (as a script hash) deployed script with the given method,
arguments and cosigners. If no method is given "" is passed to the script, if
no arguments are given, an empty array is passed, if no cosigners are given,
no array will be passed. All of the given arguments are encapsulated into
array before invoking the script. The script thus should follow the regular
convention of smart contract arguments (method string and an array of other
arguments).
Arguments always do have regular Neo smart contract parameter types, either
specified explicitly or being inferred from the value. To specify the type
@ -214,6 +219,32 @@ func NewCommands() []cli.Command {
* 'string\:string' is a string with a value of 'string:string'
* '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a
key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c'
Cosigners represent a set of Uint160 hashes with witness scopes and are used
to verify hashes in System.Runtime.CheckWitness syscall. To specify cosigners
use cosigner[:scope] syntax where
* 'cosigner' is hex-encoded 160 bit (20 byte) LE value of cosigner's address,
which could have '0x' prefix.
* 'scope' is a comma-separated set of cosigner's scopes, which could be:
- 'Global' - allows this witness in all contexts. This cannot be combined
with other flags.
- 'CalledByEntry' - means that this condition must hold: EntryScriptHash
== CallingScriptHash. The witness/permission/signature
given on first invocation will automatically expire if
entering deeper internal invokes. This can be default
safe choice for native NEO/GAS.
- 'CustomContracts' - define valid custom contract hashes for witness check.
- 'CustomGroups' - define custom pubkey for group members.
If no scopes were specified, 'Global' used as default. If no cosigners were
specified, no array will be passed. Note that scopes are properly handled by
neo-go RPC server only. C# implementation does not support scopes capability.
Examples:
* '0000000009070e030d0f0e020d0c06050e030c02'
* '0x0000000009070e030d0f0e020d0c06050e030c02'
* '0x0000000009070e030d0f0e020d0c06050e030c02:Global'
* '0000000009070e030d0f0e020d0c06050e030c02:CalledByEntry,CustomGroups'
`,
Action: testInvokeFunction,
Flags: []cli.Flag{
@ -221,8 +252,12 @@ func NewCommands() []cli.Command {
},
},
{
Name: "testinvokescript",
Usage: "Invoke compiled AVM code on the blockchain (test mode, not creating a transaction for it)",
Name: "testinvokescript",
Usage: "Invoke compiled AVM code on the blockchain (test mode, not creating a transaction for it)",
UsageText: "neo-go contract testinvokescript -e endpoint -i input.avm [cosigners...]",
Description: `Executes given compiled AVM instructions with the given set of
cosigners. See testinvokefunction documentation for the details about parameters.
`,
Action: testInvokeScript,
Flags: []cli.Flag{
endpointFlag,
@ -362,13 +397,15 @@ func invokeFunction(ctx *cli.Context) error {
func invokeInternal(ctx *cli.Context, signAndPush bool) error {
var (
err error
gas util.Fixed8
operation string
params = make([]smartcontract.Parameter, 0)
paramsStart = 1
resp *result.Invoke
acc *wallet.Account
err error
gas util.Fixed8
operation string
params = make([]smartcontract.Parameter, 0)
paramsStart = 1
cosigners []transaction.Cosigner
cosignersStart = 0
resp *result.Invoke
acc *wallet.Account
)
endpoint := ctx.String("endpoint")
@ -390,6 +427,10 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
if len(args) > paramsStart {
for k, s := range args[paramsStart:] {
if s == cosignersSeparator {
cosignersStart = paramsStart + k + 1
break
}
param, err := smartcontract.NewParameterFromString(s)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to parse argument #%d: %v", k+paramsStart+1, err), 1)
@ -398,6 +439,16 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
}
}
if len(args) >= cosignersStart && cosignersStart > 0 {
for i, c := range args[cosignersStart:] {
cosigner, err := parseCosigner(c)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to parse cosigner #%d: %v", i+cosignersStart+1, err), 1)
}
cosigners = append(cosigners, cosigner)
}
}
if signAndPush {
gas = flags.Fixed8FromContext(ctx, "gas")
acc, err = getAccFromContext(ctx)
@ -410,7 +461,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
return cli.NewExitError(err, 1)
}
resp, err = c.InvokeFunction(script, operation, params)
resp, err = c.InvokeFunction(script, operation, params, cosigners)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -454,13 +505,25 @@ func testInvokeScript(ctx *cli.Context) error {
return cli.NewExitError(err, 1)
}
args := ctx.Args()
var cosigners []transaction.Cosigner
if args.Present() {
for i, c := range args[:] {
cosigner, err := parseCosigner(c)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to parse cosigner #%d: %v", i+1, err), 1)
}
cosigners = append(cosigners, cosigner)
}
}
c, err := client.New(context.TODO(), endpoint, client.Options{})
if err != nil {
return cli.NewExitError(err, 1)
}
scriptHex := hex.EncodeToString(b)
resp, err := c.InvokeScript(scriptHex)
resp, err := c.InvokeScript(scriptHex, cosigners)
if err != nil {
return cli.NewExitError(err, 1)
}
@ -625,3 +688,26 @@ func parseContractConfig(confFile string) (ProjectConfig, error) {
}
return conf, nil
}
func parseCosigner(c string) (transaction.Cosigner, error) {
var (
err error
res = transaction.Cosigner{}
)
data := strings.SplitN(strings.ToLower(c), ":", 2)
s := data[0]
if len(s) == 2*util.Uint160Size+2 && s[0:2] == "0x" {
s = s[2:]
}
res.Account, err = util.Uint160DecodeStringLE(s)
if err != nil {
return res, err
}
if len(data) > 1 {
res.Scopes, err = transaction.ScopesFromString(data[1])
if err != nil {
return transaction.Cosigner{}, err
}
}
return res, nil
}

View file

@ -1,5 +1,10 @@
package transaction
import (
"fmt"
"strings"
)
// WitnessScope represents set of witness flags for Transaction cosigner.
type WitnessScope byte
@ -17,3 +22,34 @@ const (
// CustomGroups define custom pubkey for group members.
CustomGroups WitnessScope = 0x20
)
// ScopesFromString converts string of comma-separated scopes to a set of scopes
// (case doesn't matter). String can combine several scopes, e.g. be any of:
// 'Global', 'CalledByEntry,CustomGroups' etc. In case of an empty string an
// error will be returned.
func ScopesFromString(s string) (WitnessScope, error) {
var result WitnessScope
s = strings.ToLower(s)
scopes := strings.Split(s, ",")
dict := map[string]WitnessScope{
"global": Global,
"calledbyentry": CalledByEntry,
"customcontracts": CustomContracts,
"customgroups": CustomGroups,
}
var isGlobal bool
for _, scopeStr := range scopes {
scope, ok := dict[scopeStr]
if !ok {
return result, fmt.Errorf("invalid witness scope: %v", scopeStr)
}
if isGlobal && !(scope == Global) {
return result, fmt.Errorf("Global scope can not be combined with other scopes")
}
result |= scope
if scope == Global {
isGlobal = true
}
}
return result, nil
}

View file

@ -0,0 +1,45 @@
package transaction
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestScopesFromString(t *testing.T) {
s, err := ScopesFromString("")
require.Error(t, err)
_, err = ScopesFromString("123")
require.Error(t, err)
s, err = ScopesFromString("Global")
require.NoError(t, err)
require.Equal(t, Global, s)
s, err = ScopesFromString("CalledByEntry")
require.NoError(t, err)
require.Equal(t, CalledByEntry, s)
s, err = ScopesFromString("CustomContracts")
require.NoError(t, err)
require.Equal(t, CustomContracts, s)
s, err = ScopesFromString("CustomGroups")
require.NoError(t, err)
require.Equal(t, CustomGroups, s)
s, err = ScopesFromString("Calledbyentry,customgroups")
require.NoError(t, err)
require.Equal(t, CalledByEntry|CustomGroups, s)
_, err = ScopesFromString("global,customgroups")
require.Error(t, err)
_, err = ScopesFromString("calledbyentry,global,customgroups")
require.Error(t, err)
s, err = ScopesFromString("Calledbyentry,customgroups,Customgroups")
require.NoError(t, err)
require.Equal(t, CalledByEntry|CustomGroups, s)
}

View file

@ -18,7 +18,7 @@ import (
// NEP5Decimals invokes `decimals` NEP5 method on a specified contract.
func (c *Client) NEP5Decimals(tokenHash util.Uint160) (int64, error) {
result, err := c.InvokeFunction(tokenHash.StringLE(), "decimals", []smartcontract.Parameter{})
result, err := c.InvokeFunction(tokenHash.StringLE(), "decimals", []smartcontract.Parameter{}, nil)
if err != nil {
return 0, err
} else if result.State != "HALT" || len(result.Stack) == 0 {
@ -30,7 +30,7 @@ func (c *Client) NEP5Decimals(tokenHash util.Uint160) (int64, error) {
// NEP5Name invokes `name` NEP5 method on a specified contract.
func (c *Client) NEP5Name(tokenHash util.Uint160) (string, error) {
result, err := c.InvokeFunction(tokenHash.StringLE(), "name", []smartcontract.Parameter{})
result, err := c.InvokeFunction(tokenHash.StringLE(), "name", []smartcontract.Parameter{}, nil)
if err != nil {
return "", err
} else if result.State != "HALT" || len(result.Stack) == 0 {
@ -42,7 +42,7 @@ func (c *Client) NEP5Name(tokenHash util.Uint160) (string, error) {
// NEP5Symbol invokes `symbol` NEP5 method on a specified contract.
func (c *Client) NEP5Symbol(tokenHash util.Uint160) (string, error) {
result, err := c.InvokeFunction(tokenHash.StringLE(), "symbol", []smartcontract.Parameter{})
result, err := c.InvokeFunction(tokenHash.StringLE(), "symbol", []smartcontract.Parameter{}, nil)
if err != nil {
return "", err
} else if result.State != "HALT" || len(result.Stack) == 0 {
@ -54,7 +54,7 @@ func (c *Client) NEP5Symbol(tokenHash util.Uint160) (string, error) {
// NEP5TotalSupply invokes `totalSupply` NEP5 method on a specified contract.
func (c *Client) NEP5TotalSupply(tokenHash util.Uint160) (int64, error) {
result, err := c.InvokeFunction(tokenHash.StringLE(), "totalSupply", []smartcontract.Parameter{})
result, err := c.InvokeFunction(tokenHash.StringLE(), "totalSupply", []smartcontract.Parameter{}, nil)
if err != nil {
return 0, err
} else if result.State != "HALT" || len(result.Stack) == 0 {
@ -66,7 +66,7 @@ func (c *Client) NEP5TotalSupply(tokenHash util.Uint160) (int64, error) {
// NEP5BalanceOf invokes `balanceOf` NEP5 method on a specified contract.
func (c *Client) NEP5BalanceOf(tokenHash util.Uint160) (int64, error) {
result, err := c.InvokeFunction(tokenHash.StringLE(), "balanceOf", []smartcontract.Parameter{})
result, err := c.InvokeFunction(tokenHash.StringLE(), "balanceOf", []smartcontract.Parameter{}, nil)
if err != nil {
return 0, err
} else if result.State != "HALT" || len(result.Stack) == 0 {
@ -120,7 +120,12 @@ func (c *Client) CreateNEP5TransferTx(acc *wallet.Account, to util.Uint160, toke
},
}
result, err := c.InvokeScript(hex.EncodeToString(script))
result, err := c.InvokeScript(hex.EncodeToString(script), []transaction.Cosigner{
{
Account: from,
Scopes: transaction.Global,
},
})
if err != nil {
return nil, fmt.Errorf("can't add system fee to transaction: %v", err)
}

View file

@ -338,11 +338,16 @@ func (c *Client) GetVersion() (*result.Version, error) {
// InvokeScript returns the result of the given script after running it true the VM.
// NOTE: This is a test invoke and will not affect the blockchain.
func (c *Client) InvokeScript(script string) (*result.Invoke, error) {
func (c *Client) InvokeScript(script string, cosigners []transaction.Cosigner) (*result.Invoke, error) {
var (
params = request.NewRawParams(script)
params request.RawParams
resp = &result.Invoke{}
)
if cosigners != nil {
params = request.NewRawParams(script, cosigners)
} else {
params = request.NewRawParams(script)
}
if err := c.performRequest("invokescript", params, resp); err != nil {
return nil, err
}
@ -352,11 +357,16 @@ func (c *Client) InvokeScript(script string) (*result.Invoke, error) {
// InvokeFunction returns the results after calling the smart contract scripthash
// with the given operation and parameters.
// NOTE: this is test invoke and will not affect the blockchain.
func (c *Client) InvokeFunction(script, operation string, params []smartcontract.Parameter) (*result.Invoke, error) {
func (c *Client) InvokeFunction(script, operation string, params []smartcontract.Parameter, cosigners []transaction.Cosigner) (*result.Invoke, error) {
var (
p = request.NewRawParams(script, operation, params)
p request.RawParams
resp = &result.Invoke{}
)
if cosigners != nil {
p = request.NewRawParams(script, operation, params, cosigners)
} else {
p = request.NewRawParams(script, operation, params)
}
if err := c.performRequest("invokefunction", p, resp); err != nil {
return nil, err
}

View file

@ -565,7 +565,9 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
Type: smartcontract.Hash160Type,
Value: hash,
},
})
}, []transaction.Cosigner{{
Account: util.Uint160{1, 2, 3},
}})
},
serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"1426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf","state":"HALT","gas_consumed":"0.311","stack":[{"type":"ByteArray","value":"JivsCEQy"}],"tx":"d101361426ae7c6c9861ec418468c1f0fdc4a7f2963eb89151c10962616c616e63654f6667be39e7b562f60cbfe2aebca375a2e5ee28737caf000000000000000000000000"}}`,
result: func(c *Client) interface{} {
@ -591,7 +593,9 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
{
name: "positive",
invoke: func(c *Client) (interface{}, error) {
return c.InvokeScript("00046e616d656724058e5e1b6008847cd662728549088a9ee82191")
return c.InvokeScript("00046e616d656724058e5e1b6008847cd662728549088a9ee82191", []transaction.Cosigner{{
Account: util.Uint160{1, 2, 3},
}})
},
serverResponse: `{"jsonrpc":"2.0","id":1,"result":{"script":"00046e616d656724058e5e1b6008847cd662728549088a9ee82191","state":"HALT","gas_consumed":"0.161","stack":[{"type":"ByteArray","value":"TkVQNSBHQVM="}],"tx":"d1011b00046e616d656724058e5e1b6008847cd662728549088a9ee82191000000000000000000000000"}}`,
result: func(c *Client) interface{} {
@ -876,13 +880,13 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
{
name: "invokefunction_invalid_params_error",
invoke: func(c *Client) (interface{}, error) {
return c.InvokeFunction("", "", []smartcontract.Parameter{})
return c.InvokeFunction("", "", []smartcontract.Parameter{}, nil)
},
},
{
name: "invokescript_invalid_params_error",
invoke: func(c *Client) (interface{}, error) {
return c.InvokeScript("")
return c.InvokeScript("", nil)
},
},
{
@ -1052,13 +1056,13 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
{
name: "invokefunction_unmarshalling_error",
invoke: func(c *Client) (interface{}, error) {
return c.InvokeFunction("", "", []smartcontract.Parameter{})
return c.InvokeFunction("", "", []smartcontract.Parameter{}, nil)
},
},
{
name: "invokescript_unmarshalling_error",
invoke: func(c *Client) (interface{}, error) {
return c.InvokeScript("")
return c.InvokeScript("", nil)
},
},
{