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/go-yaml/yaml"
"github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/pkg/compiler" "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/crypto/hash"
"github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/rpc/client" "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{}) { func Main(op string, args []interface{}) {
runtime.Notify("Hello world!") 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. // NewCommands returns 'contract' command.
@ -135,11 +139,11 @@ func NewCommands() []cli.Command {
{ {
Name: "invokefunction", Name: "invokefunction",
Usage: "invoke deployed contract on the blockchain", Usage: "invoke deployed contract on the blockchain",
UsageText: "neo-go contract invokefunction -e endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...]", 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 and Description: `Executes given (as a script hash) deployed script with the given method,
and arguments. See testinvokefunction documentation for the details about arguments and cosigners. See testinvokefunction documentation for the details
parameters. It differs from testinvokefunction in that this command sends an about parameters. It differs from testinvokefunction in that this command
invocation transaction to the network. sends an invocation transaction to the network.
`, `,
Action: invokeFunction, Action: invokeFunction,
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -152,13 +156,14 @@ func NewCommands() []cli.Command {
{ {
Name: "testinvokefunction", Name: "testinvokefunction",
Usage: "invoke deployed contract on the blockchain (test mode)", Usage: "invoke deployed contract on the blockchain (test mode)",
UsageText: "neo-go contract testinvokefunction -e endpoint scripthash [method] [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 and Description: `Executes given (as a script hash) deployed script with the given method,
arguments. If no method is given "" is passed to the script, if no arguments arguments and cosigners. If no method is given "" is passed to the script, if
are given, an empty array is passed. All of the given arguments are no arguments are given, an empty array is passed, if no cosigners are given,
encapsulated into array before invoking the script. The script thus should no array will be passed. All of the given arguments are encapsulated into
follow the regular convention of smart contract arguments (method string and array before invoking the script. The script thus should follow the regular
an array of other arguments). convention of smart contract arguments (method string and an array of other
arguments).
Arguments always do have regular Neo smart contract parameter types, either Arguments always do have regular Neo smart contract parameter types, either
specified explicitly or being inferred from the value. To specify the type 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' * 'string\:string' is a string with a value of 'string:string'
* '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a * '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' is a
key with a value of '03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c' 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, Action: testInvokeFunction,
Flags: []cli.Flag{ Flags: []cli.Flag{
@ -221,8 +252,12 @@ func NewCommands() []cli.Command {
}, },
}, },
{ {
Name: "testinvokescript", Name: "testinvokescript",
Usage: "Invoke compiled AVM code on the blockchain (test mode, not creating a transaction for it)", 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, Action: testInvokeScript,
Flags: []cli.Flag{ Flags: []cli.Flag{
endpointFlag, endpointFlag,
@ -362,13 +397,15 @@ func invokeFunction(ctx *cli.Context) error {
func invokeInternal(ctx *cli.Context, signAndPush bool) error { func invokeInternal(ctx *cli.Context, signAndPush bool) error {
var ( var (
err error err error
gas util.Fixed8 gas util.Fixed8
operation string operation string
params = make([]smartcontract.Parameter, 0) params = make([]smartcontract.Parameter, 0)
paramsStart = 1 paramsStart = 1
resp *result.Invoke cosigners []transaction.Cosigner
acc *wallet.Account cosignersStart = 0
resp *result.Invoke
acc *wallet.Account
) )
endpoint := ctx.String("endpoint") endpoint := ctx.String("endpoint")
@ -390,6 +427,10 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
if len(args) > paramsStart { if len(args) > paramsStart {
for k, s := range args[paramsStart:] { for k, s := range args[paramsStart:] {
if s == cosignersSeparator {
cosignersStart = paramsStart + k + 1
break
}
param, err := smartcontract.NewParameterFromString(s) param, err := smartcontract.NewParameterFromString(s)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("failed to parse argument #%d: %v", k+paramsStart+1, err), 1) 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 { if signAndPush {
gas = flags.Fixed8FromContext(ctx, "gas") gas = flags.Fixed8FromContext(ctx, "gas")
acc, err = getAccFromContext(ctx) acc, err = getAccFromContext(ctx)
@ -410,7 +461,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
resp, err = c.InvokeFunction(script, operation, params) resp, err = c.InvokeFunction(script, operation, params, cosigners)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
@ -454,13 +505,25 @@ func testInvokeScript(ctx *cli.Context) error {
return cli.NewExitError(err, 1) 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{}) c, err := client.New(context.TODO(), endpoint, client.Options{})
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
scriptHex := hex.EncodeToString(b) scriptHex := hex.EncodeToString(b)
resp, err := c.InvokeScript(scriptHex) resp, err := c.InvokeScript(scriptHex, cosigners)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
@ -625,3 +688,26 @@ func parseContractConfig(confFile string) (ProjectConfig, error) {
} }
return conf, nil 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 package transaction
import (
"fmt"
"strings"
)
// WitnessScope represents set of witness flags for Transaction cosigner. // WitnessScope represents set of witness flags for Transaction cosigner.
type WitnessScope byte type WitnessScope byte
@ -17,3 +22,34 @@ const (
// CustomGroups define custom pubkey for group members. // CustomGroups define custom pubkey for group members.
CustomGroups WitnessScope = 0x20 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. // NEP5Decimals invokes `decimals` NEP5 method on a specified contract.
func (c *Client) NEP5Decimals(tokenHash util.Uint160) (int64, error) { 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 { if err != nil {
return 0, err return 0, err
} else if result.State != "HALT" || len(result.Stack) == 0 { } 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. // NEP5Name invokes `name` NEP5 method on a specified contract.
func (c *Client) NEP5Name(tokenHash util.Uint160) (string, error) { 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 { if err != nil {
return "", err return "", err
} else if result.State != "HALT" || len(result.Stack) == 0 { } 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. // NEP5Symbol invokes `symbol` NEP5 method on a specified contract.
func (c *Client) NEP5Symbol(tokenHash util.Uint160) (string, error) { 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 { if err != nil {
return "", err return "", err
} else if result.State != "HALT" || len(result.Stack) == 0 { } 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. // NEP5TotalSupply invokes `totalSupply` NEP5 method on a specified contract.
func (c *Client) NEP5TotalSupply(tokenHash util.Uint160) (int64, error) { 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 { if err != nil {
return 0, err return 0, err
} else if result.State != "HALT" || len(result.Stack) == 0 { } 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. // NEP5BalanceOf invokes `balanceOf` NEP5 method on a specified contract.
func (c *Client) NEP5BalanceOf(tokenHash util.Uint160) (int64, error) { 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 { if err != nil {
return 0, err return 0, err
} else if result.State != "HALT" || len(result.Stack) == 0 { } 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 { if err != nil {
return nil, fmt.Errorf("can't add system fee to transaction: %v", err) 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. // 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. // 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 ( var (
params = request.NewRawParams(script) params request.RawParams
resp = &result.Invoke{} 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 { if err := c.performRequest("invokescript", params, resp); err != nil {
return nil, err 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 // InvokeFunction returns the results after calling the smart contract scripthash
// with the given operation and parameters. // with the given operation and parameters.
// NOTE: this is test invoke and will not affect the blockchain. // 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 ( var (
p = request.NewRawParams(script, operation, params) p request.RawParams
resp = &result.Invoke{} 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 { if err := c.performRequest("invokefunction", p, resp); err != nil {
return nil, err return nil, err
} }

View file

@ -565,7 +565,9 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
Type: smartcontract.Hash160Type, Type: smartcontract.Hash160Type,
Value: hash, 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"}}`, 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{} { result: func(c *Client) interface{} {
@ -591,7 +593,9 @@ var rpcClientTestCases = map[string][]rpcClientTestCase{
{ {
name: "positive", name: "positive",
invoke: func(c *Client) (interface{}, error) { 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"}}`, 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{} { result: func(c *Client) interface{} {
@ -876,13 +880,13 @@ var rpcClientErrorCases = map[string][]rpcClientErrorCase{
{ {
name: "invokefunction_invalid_params_error", name: "invokefunction_invalid_params_error",
invoke: func(c *Client) (interface{}, error) { invoke: func(c *Client) (interface{}, error) {
return c.InvokeFunction("", "", []smartcontract.Parameter{}) return c.InvokeFunction("", "", []smartcontract.Parameter{}, nil)
}, },
}, },
{ {
name: "invokescript_invalid_params_error", name: "invokescript_invalid_params_error",
invoke: func(c *Client) (interface{}, 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", name: "invokefunction_unmarshalling_error",
invoke: func(c *Client) (interface{}, error) { invoke: func(c *Client) (interface{}, error) {
return c.InvokeFunction("", "", []smartcontract.Parameter{}) return c.InvokeFunction("", "", []smartcontract.Parameter{}, nil)
}, },
}, },
{ {
name: "invokescript_unmarshalling_error", name: "invokescript_unmarshalling_error",
invoke: func(c *Client) (interface{}, error) { invoke: func(c *Client) (interface{}, error) {
return c.InvokeScript("") return c.InvokeScript("", nil)
}, },
}, },
{ {