From 3c5a720e3a8423b49f8d24966cc46c84937fc7f8 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 27 Jul 2022 19:00:36 +0300 Subject: [PATCH 1/8] smartcontract: drop Params type and TryParse methods They were first introduced in a058598ecc0e82d4caf9f6225606d9ac61fb672e and then carefully moved in 648e0bb242da9daaeef4f1bbbad69367fc26c836, but it looks like they were never used by any external code. This code can be useful on the server, but the server has its own params package to deal with parameters. Clients usually create Parameters and then get results as stackitem.Items, so they don't use this code either. So there is zero point in keeping it. --- pkg/rpcclient/rpc.go | 2 +- pkg/services/rpcsrv/client_test.go | 20 ++--- pkg/smartcontract/parameter.go | 120 ---------------------------- pkg/smartcontract/parameter_test.go | 84 ------------------- 4 files changed, 11 insertions(+), 215 deletions(-) diff --git a/pkg/rpcclient/rpc.go b/pkg/rpcclient/rpc.go index abfed089f..049f46a46 100644 --- a/pkg/rpcclient/rpc.go +++ b/pkg/rpcclient/rpc.go @@ -1047,7 +1047,7 @@ func (c *Client) AddNetworkFee(tx *transaction.Transaction, extraFee int64, accs var ef int64 for i, cosigner := range tx.Signers { if accs[i].Contract.Deployed { - res, err := c.InvokeContractVerify(cosigner.Account, smartcontract.Params{}, tx.Signers) + res, err := c.InvokeContractVerify(cosigner.Account, []smartcontract.Parameter{}, tx.Signers) if err != nil { return fmt.Errorf("failed to invoke verify: %w", err) } diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index 219963faf..4c12cf9d1 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -773,7 +773,7 @@ func TestInvokeVerify(t *testing.T) { require.NoError(t, err) t.Run("positive, with signer", func(t *testing.T) { - res, err := c.InvokeContractVerify(contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + res, err := c.InvokeContractVerify(contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) @@ -782,7 +782,7 @@ func TestInvokeVerify(t *testing.T) { t.Run("positive, historic, by height, with signer", func(t *testing.T) { h := chain.BlockHeight() - 1 - res, err := c.InvokeContractVerifyAtHeight(h, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + res, err := c.InvokeContractVerifyAtHeight(h, contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) @@ -790,7 +790,7 @@ func TestInvokeVerify(t *testing.T) { }) t.Run("positive, historic, by block, with signer", func(t *testing.T) { - res, err := c.InvokeContractVerifyAtBlock(chain.GetHeaderHash(int(chain.BlockHeight())-1), contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + res, err := c.InvokeContractVerifyAtBlock(chain.GetHeaderHash(int(chain.BlockHeight())-1), contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) @@ -801,7 +801,7 @@ func TestInvokeVerify(t *testing.T) { h := chain.BlockHeight() - 1 sr, err := chain.GetStateModule().GetStateRoot(h) require.NoError(t, err) - res, err := c.InvokeContractVerifyWithState(sr.Root, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + res, err := c.InvokeContractVerifyWithState(sr.Root, contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) @@ -810,13 +810,13 @@ func TestInvokeVerify(t *testing.T) { t.Run("bad, historic, by hash: contract not found", func(t *testing.T) { var h uint32 = 1 - _, err = c.InvokeContractVerifyAtHeight(h, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + _, err = c.InvokeContractVerifyAtHeight(h, contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.Error(t, err) require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet }) t.Run("bad, historic, by block: contract not found", func(t *testing.T) { - _, err = c.InvokeContractVerifyAtBlock(chain.GetHeaderHash(1), contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + _, err = c.InvokeContractVerifyAtBlock(chain.GetHeaderHash(1), contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.Error(t, err) require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet }) @@ -825,13 +825,13 @@ func TestInvokeVerify(t *testing.T) { var h uint32 = 1 sr, err := chain.GetStateModule().GetStateRoot(h) require.NoError(t, err) - _, err = c.InvokeContractVerifyWithState(sr.Root, contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) + _, err = c.InvokeContractVerifyWithState(sr.Root, contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}) require.Error(t, err) require.True(t, strings.Contains(err.Error(), core.ErrUnknownVerificationContract.Error())) // contract wasn't deployed at block #1 yet }) t.Run("positive, with signer and witness", func(t *testing.T) { - res, err := c.InvokeContractVerify(contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}, transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.RET)}}) + res, err := c.InvokeContractVerify(contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}, transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.RET)}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) @@ -839,12 +839,12 @@ func TestInvokeVerify(t *testing.T) { }) t.Run("error, invalid witness number", func(t *testing.T) { - _, err := c.InvokeContractVerify(contract, smartcontract.Params{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}, transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.RET)}}, transaction.Witness{InvocationScript: []byte{byte(opcode.RET)}}) + _, err := c.InvokeContractVerify(contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}}, transaction.Witness{InvocationScript: []byte{byte(opcode.PUSH1), byte(opcode.RET)}}, transaction.Witness{InvocationScript: []byte{byte(opcode.RET)}}) require.Error(t, err) }) t.Run("false", func(t *testing.T) { - res, err := c.InvokeContractVerify(contract, smartcontract.Params{}, []transaction.Signer{{Account: util.Uint160{}}}) + res, err := c.InvokeContractVerify(contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: util.Uint160{}}}) require.NoError(t, err) require.Equal(t, "HALT", res.State) require.Equal(t, 1, len(res.Stack)) diff --git a/pkg/smartcontract/parameter.go b/pkg/smartcontract/parameter.go index 97cb04a2c..3cecd259c 100644 --- a/pkg/smartcontract/parameter.go +++ b/pkg/smartcontract/parameter.go @@ -3,19 +3,16 @@ package smartcontract import ( "bytes" "encoding/base64" - "encoding/binary" "encoding/hex" "encoding/json" "errors" "fmt" "math/big" - "math/bits" "os" "strings" "unicode/utf8" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" ) @@ -194,123 +191,6 @@ func (p *Parameter) UnmarshalJSON(data []byte) (err error) { return } -// Params is an array of Parameter (TODO: drop it?). -type Params []Parameter - -// TryParseArray converts an array of Parameter into an array of more appropriate things. -func (p Params) TryParseArray(vals ...interface{}) error { - var ( - err error - i int - par Parameter - ) - if len(p) != len(vals) { - return errors.New("receiver array doesn't fit the Params length") - } - for i, par = range p { - if err = par.TryParse(vals[i]); err != nil { - return err - } - } - return nil -} - -// TryParse converts one Parameter into something more appropriate. -func (p Parameter) TryParse(dest interface{}) error { - var ( - err error - ok bool - data []byte - ) - switch p.Type { - case ByteArrayType: - if data, ok = p.Value.([]byte); !ok { - return fmt.Errorf("failed to cast %s to []byte", p.Value) - } - switch dest := dest.(type) { - case *util.Uint160: - if *dest, err = util.Uint160DecodeBytesBE(data); err != nil { - return err - } - return nil - case *[]byte: - *dest = data - return nil - case *util.Uint256: - if *dest, err = util.Uint256DecodeBytesLE(data); err != nil { - return err - } - return nil - case **big.Int: - *dest = bigint.FromBytes(data) - return nil - case *int64, *int32, *int16, *int8, *int, *uint64, *uint32, *uint16, *uint8, *uint: - var size int - switch dest.(type) { - case *int64, *uint64: - size = 64 - case *int32, *uint32: - size = 32 - case *int16, *uint16: - size = 16 - case *int8, *uint8: - size = 8 - case *int, *uint: - size = bits.UintSize - } - - i, err := bytesToUint64(data, size) - if err != nil { - return err - } - - switch dest := dest.(type) { - case *int64: - *dest = int64(i) - case *int32: - *dest = int32(i) - case *int16: - *dest = int16(i) - case *int8: - *dest = int8(i) - case *int: - *dest = int(i) - case *uint64: - *dest = i - case *uint32: - *dest = uint32(i) - case *uint16: - *dest = uint16(i) - case *uint8: - *dest = uint8(i) - case *uint: - *dest = uint(i) - } - case *string: - *dest = string(data) - return nil - default: - return fmt.Errorf("cannot cast param of type %s to type %s", p.Type, dest) - } - default: - return errors.New("cannot define param type") - } - return nil -} - -func bytesToUint64(b []byte, size int) (uint64, error) { - var length = size / 8 - if len(b) > length { - return 0, fmt.Errorf("input doesn't fit into %d bits", size) - } - if len(b) < length { - data := make([]byte, length) - copy(data, b) - return binary.LittleEndian.Uint64(data), nil - } - return binary.LittleEndian.Uint64(b), nil -} - // NewParameterFromString returns a new Parameter initialized from the given // string in neo-go-specific format. It is intended to be used in user-facing // interfaces and has some heuristics in it to simplify parameter passing. The exact diff --git a/pkg/smartcontract/parameter_test.go b/pkg/smartcontract/parameter_test.go index e52cc67a1..51b5931c4 100644 --- a/pkg/smartcontract/parameter_test.go +++ b/pkg/smartcontract/parameter_test.go @@ -6,7 +6,6 @@ import ( "encoding/json" "math" "math/big" - "reflect" "strings" "testing" @@ -341,89 +340,6 @@ func TestParam_UnmarshalJSON(t *testing.T) { } } -var tryParseTestCases = []struct { - input interface{} - expected interface{} -}{ - { - input: []byte{ - 0x0b, 0xcd, 0x29, 0x78, 0x63, 0x4d, 0x96, 0x1c, 0x24, 0xf5, - 0xae, 0xa0, 0x80, 0x22, 0x97, 0xff, 0x12, 0x87, 0x24, 0xd6, - }, - expected: util.Uint160{ - 0x0b, 0xcd, 0x29, 0x78, 0x63, 0x4d, 0x96, 0x1c, 0x24, 0xf5, - 0xae, 0xa0, 0x80, 0x22, 0x97, 0xff, 0x12, 0x87, 0x24, 0xd6, - }, - }, - { - input: []byte{ - 0xf0, 0x37, 0x30, 0x8f, 0xa0, 0xab, 0x18, 0x15, - 0x5b, 0xcc, 0xfc, 0x08, 0x48, 0x54, 0x68, 0xc1, - 0x12, 0x40, 0x9e, 0xa5, 0x06, 0x45, 0x95, 0x69, - 0x9e, 0x98, 0xc5, 0x45, 0xf2, 0x45, 0xf3, 0x2d, - }, - expected: util.Uint256{ - 0x2d, 0xf3, 0x45, 0xf2, 0x45, 0xc5, 0x98, 0x9e, - 0x69, 0x95, 0x45, 0x06, 0xa5, 0x9e, 0x40, 0x12, - 0xc1, 0x68, 0x54, 0x48, 0x08, 0xfc, 0xcc, 0x5b, - 0x15, 0x18, 0xab, 0xa0, 0x8f, 0x30, 0x37, 0xf0, - }, - }, - { - input: []byte{0, 1, 2, 3, 4, 9, 8, 6}, - expected: []byte{0, 1, 2, 3, 4, 9, 8, 6}, - }, - { - input: []byte{0x63, 0x78, 0x29, 0xcd, 0x0b}, - expected: int64(50686687331), - }, - { - input: []byte{0x63, 0x78, 0x29, 0xcd, 0x0b}, - expected: big.NewInt(50686687331), - }, - { - input: []byte("this is a test string"), - expected: "this is a test string", - }, -} - -func TestParam_TryParse(t *testing.T) { - for _, tc := range tryParseTestCases { - t.Run(reflect.TypeOf(tc.expected).String(), func(t *testing.T) { - input := Parameter{ - Type: ByteArrayType, - Value: tc.input, - } - - val := reflect.New(reflect.TypeOf(tc.expected)) - assert.NoError(t, input.TryParse(val.Interface())) - assert.Equal(t, tc.expected, val.Elem().Interface()) - }) - } - - t.Run("[]Uint160", func(t *testing.T) { - exp1 := util.Uint160{1, 2, 3, 4, 5} - exp2 := util.Uint160{9, 8, 7, 6, 5} - - params := Params{ - { - Type: ByteArrayType, - Value: exp1.BytesBE(), - }, - { - Type: ByteArrayType, - Value: exp2.BytesBE(), - }, - } - - var out1, out2 util.Uint160 - - assert.NoError(t, params.TryParseArray(&out1, &out2)) - assert.Equal(t, exp1, out1) - assert.Equal(t, exp2, out2) - }) -} - func TestParamType_String(t *testing.T) { types := []ParamType{ SignatureType, From 848d68fba8b95fdf4f3979438c8a9e4bc1f3ae5e Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Wed, 27 Jul 2022 19:04:09 +0300 Subject: [PATCH 2/8] smartcontract: improve package documentation --- pkg/smartcontract/doc.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/smartcontract/doc.go b/pkg/smartcontract/doc.go index d9292ae85..4f45fb45f 100644 --- a/pkg/smartcontract/doc.go +++ b/pkg/smartcontract/doc.go @@ -1,8 +1,10 @@ /* -Package smartcontract contains functions to deal with widely used scripts. +Package smartcontract contains functions to deal with widely used scripts and NEP-14 Parameters. Neo is all about various executed code, verifications and executions of -transactions need some NeoVM code and this package simplifies creating it +transactions need NeoVM code and this package simplifies creating it for common tasks like multisignature verification scripts or transaction -entry scripts that call previously deployed contracts. +entry scripts that call previously deployed contracts. Another problem related +to scripts and invocations is that RPC invocations use JSONized NEP-14 +parameters, so this package provides types and methods to deal with that too. */ package smartcontract From a8a2f2ed5ab9c4448d9e0b9bd4b041e728b9dc74 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 29 Jul 2022 11:11:18 +0300 Subject: [PATCH 3/8] smartcontract: make CreateCallAndUnwrapIteratorScript accept Go types Parameter is for the RPC client, all other CreateXXXScript functions deal with regular types. --- pkg/rpcclient/helper.go | 9 ++++++++- pkg/smartcontract/entry.go | 15 +++------------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/pkg/rpcclient/helper.go b/pkg/rpcclient/helper.go index 3db0052d3..6f91a5855 100644 --- a/pkg/rpcclient/helper.go +++ b/pkg/rpcclient/helper.go @@ -116,7 +116,14 @@ func (c *Client) InvokeAndPackIteratorResults(contract util.Uint160, operation s if len(maxIteratorResultItems) != 0 { max = maxIteratorResultItems[0] } - bytes, err := smartcontract.CreateCallAndUnwrapIteratorScript(contract, operation, params, max) + values, err := smartcontract.ExpandParameterToEmitable(smartcontract.Parameter{ + Type: smartcontract.ArrayType, + Value: params, + }) + if err != nil { + return nil, fmt.Errorf("expanding params to emitable: %w", err) + } + bytes, err := smartcontract.CreateCallAndUnwrapIteratorScript(contract, operation, max, values.([]interface{})...) if err != nil { return nil, fmt.Errorf("failed to create iterator unwrapper script: %w", err) } diff --git a/pkg/smartcontract/entry.go b/pkg/smartcontract/entry.go index a3fb44262..c6f6b211a 100644 --- a/pkg/smartcontract/entry.go +++ b/pkg/smartcontract/entry.go @@ -18,20 +18,11 @@ import ( // processed this way (and this number can't exceed VM limits), the result of the // script is an array containing extracted value elements. This script can be useful // for interactions with RPC server that have iterator sessions disabled. -func CreateCallAndUnwrapIteratorScript(contract util.Uint160, operation string, params []Parameter, maxIteratorResultItems int) ([]byte, error) { +func CreateCallAndUnwrapIteratorScript(contract util.Uint160, operation string, maxIteratorResultItems int, params ...interface{}) ([]byte, error) { script := io.NewBufBinWriter() emit.Int(script.BinWriter, int64(maxIteratorResultItems)) - // Pack arguments for System.Contract.Call. - arr, err := ExpandParameterToEmitable(Parameter{ - Type: ArrayType, - Value: params, - }) - if err != nil { - return nil, fmt.Errorf("expanding params to emitable: %w", err) - } - emit.Array(script.BinWriter, arr.([]interface{})...) - emit.AppCallNoArgs(script.BinWriter, contract, operation, callflag.All) // The System.Contract.Call itself, it will push Iterator on estack. - emit.Opcodes(script.BinWriter, opcode.NEWARRAY0) // Push new empty array to estack. This array will store iterator's elements. + emit.AppCall(script.BinWriter, contract, operation, callflag.All, params...) // The System.Contract.Call itself, it will push Iterator on estack. + emit.Opcodes(script.BinWriter, opcode.NEWARRAY0) // Push new empty array to estack. This array will store iterator's elements. // Start the iterator traversal cycle. iteratorTraverseCycleStartOffset := script.Len() From 92a931c145f08fc7f91bd986da4a1e9c96c88e06 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 29 Jul 2022 12:24:12 +0300 Subject: [PATCH 4/8] smartcontract: provide interface{}->Parameter conversion Which is almost like a NeoFS's toStackParameter() on steroids (except it doesn't mess with noderoles package, it can be casted to int). RPC client's Invoke* functions expect Parameters, so make it easy to create them. --- pkg/smartcontract/parameter.go | 95 ++++++++++++++++ pkg/smartcontract/parameter_test.go | 165 ++++++++++++++++++++++++++++ 2 files changed, 260 insertions(+) diff --git a/pkg/smartcontract/parameter.go b/pkg/smartcontract/parameter.go index 3cecd259c..170a59028 100644 --- a/pkg/smartcontract/parameter.go +++ b/pkg/smartcontract/parameter.go @@ -255,6 +255,101 @@ func NewParameterFromString(in string) (*Parameter, error) { return res, nil } +// NewParameterFromValue infers Parameter type from the value given and adjusts +// the value if needed. It does not copy the value if it can avoid doing so. All +// regular integers, util.*, keys.PublicKey*, string and bool types are supported, +// slice of byte slices is accepted and converted as well. +func NewParameterFromValue(value interface{}) (Parameter, error) { + var result = Parameter{ + Value: value, + } + + switch v := value.(type) { + case []byte: + result.Type = ByteArrayType + case string: + result.Type = StringType + case bool: + result.Type = BoolType + case *big.Int: + result.Type = IntegerType + case int8: + result.Type = IntegerType + result.Value = big.NewInt(int64(v)) + case byte: + result.Type = IntegerType + result.Value = big.NewInt(int64(v)) + case int16: + result.Type = IntegerType + result.Value = big.NewInt(int64(v)) + case uint16: + result.Type = IntegerType + result.Value = big.NewInt(int64(v)) + case int32: + result.Type = IntegerType + result.Value = big.NewInt(int64(v)) + case uint32: + result.Type = IntegerType + result.Value = big.NewInt(int64(v)) + case int: + result.Type = IntegerType + result.Value = big.NewInt(int64(v)) + case uint: + result.Type = IntegerType + result.Value = new(big.Int).SetUint64(uint64(v)) + case int64: + result.Type = IntegerType + result.Value = big.NewInt(v) + case uint64: + result.Type = IntegerType + result.Value = new(big.Int).SetUint64(v) + case util.Uint160: + result.Type = Hash160Type + case util.Uint256: + result.Type = Hash256Type + case keys.PublicKey: + return NewParameterFromValue(&v) + case *keys.PublicKey: + result.Type = PublicKeyType + result.Value = v.Bytes() + case [][]byte: + arr := make([]Parameter, 0, len(v)) + for i := range v { + // We know the type exactly, so error is not possible. + elem, _ := NewParameterFromValue(v[i]) + arr = append(arr, elem) + } + result.Type = ArrayType + result.Value = arr + case []*keys.PublicKey: + return NewParameterFromValue(keys.PublicKeys(v)) + case keys.PublicKeys: + arr := make([]Parameter, 0, len(v)) + for i := range v { + // We know the type exactly, so error is not possible. + elem, _ := NewParameterFromValue(v[i]) + arr = append(arr, elem) + } + result.Type = ArrayType + result.Value = arr + case []interface{}: + arr := make([]Parameter, 0, len(v)) + for i := range v { + elem, err := NewParameterFromValue(v[i]) + if err != nil { + return result, err + } + arr = append(arr, elem) + } + result.Type = ArrayType + result.Value = arr + default: + return result, fmt.Errorf("unsupported parameter %T", value) + } + + return result, nil +} + // ExpandParameterToEmitable converts a parameter to a type which can be handled as // an array item by emit.Array. It correlates with the way an RPC server handles // FuncParams for invoke* calls inside the request.ExpandArrayIntoScript function. diff --git a/pkg/smartcontract/parameter_test.go b/pkg/smartcontract/parameter_test.go index 51b5931c4..281230ffd 100644 --- a/pkg/smartcontract/parameter_test.go +++ b/pkg/smartcontract/parameter_test.go @@ -527,3 +527,168 @@ func TestExpandParameterToEmitable(t *testing.T) { require.Error(t, err) } } + +func TestParameterFromValue(t *testing.T) { + pk1, _ := keys.NewPrivateKey() + pk2, _ := keys.NewPrivateKey() + items := []struct { + value interface{} + expType ParamType + expVal interface{} + }{ + { + value: []byte{1, 2, 3}, + expType: ByteArrayType, + expVal: []byte{1, 2, 3}, + }, + { + value: "hello world", + expType: StringType, + expVal: "hello world", + }, + { + value: false, + expType: BoolType, + expVal: false, + }, + { + value: true, + expType: BoolType, + expVal: true, + }, + { + value: big.NewInt(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: byte(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: int8(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: uint8(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: int16(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: uint16(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: int32(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: uint32(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: 100, + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: uint(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: int64(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: uint64(100), + expType: IntegerType, + expVal: big.NewInt(100), + }, + { + value: util.Uint160{1, 2, 3}, + expType: Hash160Type, + expVal: util.Uint160{1, 2, 3}, + }, + { + value: util.Uint256{3, 2, 1}, + expType: Hash256Type, + expVal: util.Uint256{3, 2, 1}, + }, + { + value: pk1.PublicKey(), + expType: PublicKeyType, + expVal: pk1.PublicKey().Bytes(), + }, + { + value: *pk2.PublicKey(), + expType: PublicKeyType, + expVal: pk2.PublicKey().Bytes(), + }, + { + value: [][]byte{{1, 2, 3}, {3, 2, 1}}, + expType: ArrayType, + expVal: []Parameter{{ByteArrayType, []byte{1, 2, 3}}, {ByteArrayType, []byte{3, 2, 1}}}, + }, + { + value: []*keys.PublicKey{pk1.PublicKey(), pk2.PublicKey()}, + expType: ArrayType, + expVal: []Parameter{{ + Type: PublicKeyType, + Value: pk1.PublicKey().Bytes(), + }, { + Type: PublicKeyType, + Value: pk2.PublicKey().Bytes(), + }}, + }, + { + value: keys.PublicKeys{pk1.PublicKey(), pk2.PublicKey()}, + expType: ArrayType, + expVal: []Parameter{{ + Type: PublicKeyType, + Value: pk1.PublicKey().Bytes(), + }, { + Type: PublicKeyType, + Value: pk2.PublicKey().Bytes(), + }}, + }, + { + value: []interface{}{-42, "random", []byte{1, 2, 3}}, + expType: ArrayType, + expVal: []Parameter{{ + Type: IntegerType, + Value: big.NewInt(-42), + }, { + Type: StringType, + Value: "random", + }, { + Type: ByteArrayType, + Value: []byte{1, 2, 3}, + }}, + }, + } + + for _, item := range items { + t.Run(item.expType.String()+" to stack parameter", func(t *testing.T) { + res, err := NewParameterFromValue(item.value) + require.NoError(t, err) + require.Equal(t, item.expType, res.Type) + require.Equal(t, item.expVal, res.Value) + }) + } + _, err := NewParameterFromValue(make(map[string]int)) + require.Error(t, err) + _, err = NewParameterFromValue([]interface{}{1, 2, make(map[string]int)}) + require.Error(t, err) +} From 55164132df11715a1371de320a4b53621e5ff449 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 29 Jul 2022 12:38:33 +0300 Subject: [PATCH 5/8] smartcontract: provide NewParametersFromValues for convenience as well --- pkg/smartcontract/parameter.go | 24 +++++++++++++++++------- pkg/smartcontract/parameter_test.go | 17 +++++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/pkg/smartcontract/parameter.go b/pkg/smartcontract/parameter.go index 170a59028..e4496fd20 100644 --- a/pkg/smartcontract/parameter.go +++ b/pkg/smartcontract/parameter.go @@ -333,13 +333,9 @@ func NewParameterFromValue(value interface{}) (Parameter, error) { result.Type = ArrayType result.Value = arr case []interface{}: - arr := make([]Parameter, 0, len(v)) - for i := range v { - elem, err := NewParameterFromValue(v[i]) - if err != nil { - return result, err - } - arr = append(arr, elem) + arr, err := NewParametersFromValues(v...) + if err != nil { + return result, err } result.Type = ArrayType result.Value = arr @@ -350,6 +346,20 @@ func NewParameterFromValue(value interface{}) (Parameter, error) { return result, nil } +// NewParametersFromValues is similar to NewParameterFromValue except that it +// works with multiple values and returns a simple slice of Parameter. +func NewParametersFromValues(values ...interface{}) ([]Parameter, error) { + res := make([]Parameter, 0, len(values)) + for i := range values { + elem, err := NewParameterFromValue(values[i]) + if err != nil { + return nil, err + } + res = append(res, elem) + } + return res, nil +} + // ExpandParameterToEmitable converts a parameter to a type which can be handled as // an array item by emit.Array. It correlates with the way an RPC server handles // FuncParams for invoke* calls inside the request.ExpandArrayIntoScript function. diff --git a/pkg/smartcontract/parameter_test.go b/pkg/smartcontract/parameter_test.go index 281230ffd..181901876 100644 --- a/pkg/smartcontract/parameter_test.go +++ b/pkg/smartcontract/parameter_test.go @@ -692,3 +692,20 @@ func TestParameterFromValue(t *testing.T) { _, err = NewParameterFromValue([]interface{}{1, 2, make(map[string]int)}) require.Error(t, err) } + +func TestParametersFromValues(t *testing.T) { + res, err := NewParametersFromValues(42, "some", []byte{3, 2, 1}) + require.NoError(t, err) + require.Equal(t, []Parameter{{ + Type: IntegerType, + Value: big.NewInt(42), + }, { + Type: StringType, + Value: "some", + }, { + Type: ByteArrayType, + Value: []byte{3, 2, 1}, + }}, res) + _, err = NewParametersFromValues(42, make(map[int]int), []byte{3, 2, 1}) + require.Error(t, err) +} From fee7e2f2235db8cffaff39fe03955673b22f03ee Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 29 Jul 2022 22:02:25 +0300 Subject: [PATCH 6/8] rpcclient: add invoker package and structure --- pkg/rpcclient/invoker/invoker.go | 141 ++++++++++++++++++++++++++ pkg/rpcclient/invoker/invoker_test.go | 108 ++++++++++++++++++++ 2 files changed, 249 insertions(+) create mode 100644 pkg/rpcclient/invoker/invoker.go create mode 100644 pkg/rpcclient/invoker/invoker_test.go diff --git a/pkg/rpcclient/invoker/invoker.go b/pkg/rpcclient/invoker/invoker.go new file mode 100644 index 000000000..8336449b4 --- /dev/null +++ b/pkg/rpcclient/invoker/invoker.go @@ -0,0 +1,141 @@ +package invoker + +import ( + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" +) + +// RPCInvoke is a set of RPC methods needed to execute things at the current +// blockchain height. +type RPCInvoke interface { + InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) + InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) + InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) +} + +// RPCInvokeHistoric is a set of RPC methods needed to execute things at some +// fixed point in blockchain's life. +type RPCInvokeHistoric interface { + InvokeContractVerifyAtBlock(blockHash util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) + InvokeContractVerifyAtHeight(height uint32, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) + InvokeContractVerifyWithState(stateroot util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) + InvokeFunctionAtBlock(blockHash util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) + InvokeFunctionAtHeight(height uint32, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) + InvokeFunctionWithState(stateroot util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) + InvokeScriptAtBlock(blockHash util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) + InvokeScriptAtHeight(height uint32, script []byte, signers []transaction.Signer) (*result.Invoke, error) + InvokeScriptWithState(stateroot util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) +} + +// Invoker allows to test-execute things using RPC client. Its API simplifies +// reusing the same signers list for a series of invocations and at the +// same time uses regular Go types for call parameters. It doesn't do anything with +// the result of invocation, that's left for upper (contract) layer to deal with. +// Invoker does not produce any transactions and does not change the state of the +// chain. +type Invoker struct { + client RPCInvoke + signers []transaction.Signer +} + +type historicConverter struct { + client RPCInvokeHistoric + block *util.Uint256 + height *uint32 + root *util.Uint256 +} + +// New creates an Invoker to test-execute things at the current blockchain height. +func New(client RPCInvoke, signers []transaction.Signer) *Invoker { + return &Invoker{client, signers} +} + +// NewHistoricAtBlock creates an Invoker to test-execute things at some given block. +func NewHistoricAtBlock(block util.Uint256, client RPCInvokeHistoric, signers []transaction.Signer) *Invoker { + return New(&historicConverter{ + client: client, + block: &block, + }, signers) +} + +// NewHistoricAtHeight creates an Invoker to test-execute things at some given height. +func NewHistoricAtHeight(height uint32, client RPCInvokeHistoric, signers []transaction.Signer) *Invoker { + return New(&historicConverter{ + client: client, + height: &height, + }, signers) +} + +// NewHistoricWithState creates an Invoker to test-execute things with some given state. +func NewHistoricWithState(root util.Uint256, client RPCInvokeHistoric, signers []transaction.Signer) *Invoker { + return New(&historicConverter{ + client: client, + root: &root, + }, signers) +} + +func (h *historicConverter) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) { + if h.block != nil { + return h.client.InvokeScriptAtBlock(*h.block, script, signers) + } + if h.height != nil { + return h.client.InvokeScriptAtHeight(*h.height, script, signers) + } + if h.root != nil { + return h.client.InvokeScriptWithState(*h.root, script, signers) + } + panic("uninitialized historicConverter") +} + +func (h *historicConverter) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + if h.block != nil { + return h.client.InvokeFunctionAtBlock(*h.block, contract, operation, params, signers) + } + if h.height != nil { + return h.client.InvokeFunctionAtHeight(*h.height, contract, operation, params, signers) + } + if h.root != nil { + return h.client.InvokeFunctionWithState(*h.root, contract, operation, params, signers) + } + panic("uninitialized historicConverter") +} + +func (h *historicConverter) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + if h.block != nil { + return h.client.InvokeContractVerifyAtBlock(*h.block, contract, params, signers, witnesses...) + } + if h.height != nil { + return h.client.InvokeContractVerifyAtHeight(*h.height, contract, params, signers, witnesses...) + } + if h.root != nil { + return h.client.InvokeContractVerifyWithState(*h.root, contract, params, signers, witnesses...) + } + panic("uninitialized historicConverter") +} + +// Call invokes a method of the contract with the given parameters (and +// Invoker-specific list of signers) and returns the result as is. +func (v *Invoker) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) { + ps, err := smartcontract.NewParametersFromValues(params...) + if err != nil { + return nil, err + } + return v.client.InvokeFunction(contract, operation, ps, v.signers) +} + +// Verify invokes contract's verify method in the verification context with +// Invoker-specific signers and given witnesses and parameters. +func (v *Invoker) Verify(contract util.Uint160, witnesses []transaction.Witness, params ...interface{}) (*result.Invoke, error) { + ps, err := smartcontract.NewParametersFromValues(params...) + if err != nil { + return nil, err + } + return v.client.InvokeContractVerify(contract, ps, v.signers, witnesses...) +} + +// Run executes given bytecode with Invoker-specific list of signers. +func (v *Invoker) Run(script []byte) (*result.Invoke, error) { + return v.client.InvokeScript(script, v.signers) +} diff --git a/pkg/rpcclient/invoker/invoker_test.go b/pkg/rpcclient/invoker/invoker_test.go new file mode 100644 index 000000000..fd755e421 --- /dev/null +++ b/pkg/rpcclient/invoker/invoker_test.go @@ -0,0 +1,108 @@ +package invoker + +import ( + "testing" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/neorpc/result" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/stretchr/testify/require" +) + +type rpcInv struct { + resInv *result.Invoke + err error +} + +func (r *rpcInv) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeContractVerifyAtBlock(blockHash util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeContractVerifyAtHeight(height uint32, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeContractVerifyWithState(stateroot util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeFunctionAtBlock(blockHash util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeFunctionAtHeight(height uint32, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeFunctionWithState(stateroot util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeScriptAtBlock(blockHash util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeScriptAtHeight(height uint32, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + return r.resInv, r.err +} +func (r *rpcInv) InvokeScriptWithState(stateroot util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error) { + return r.resInv, r.err +} + +func TestInvoker(t *testing.T) { + resExp := &result.Invoke{State: "HALT"} + ri := &rpcInv{resExp, nil} + + testInv := func(t *testing.T, inv *Invoker) { + res, err := inv.Call(util.Uint160{}, "method") + require.NoError(t, err) + require.Equal(t, resExp, res) + + res, err = inv.Verify(util.Uint160{}, nil) + require.NoError(t, err) + require.Equal(t, resExp, res) + + res, err = inv.Run([]byte{1}) + require.NoError(t, err) + require.Equal(t, resExp, res) + + res, err = inv.Call(util.Uint160{}, "method") + require.NoError(t, err) + require.Equal(t, resExp, res) + + res, err = inv.Verify(util.Uint160{}, nil, "param") + require.NoError(t, err) + require.Equal(t, resExp, res) + + res, err = inv.Call(util.Uint160{}, "method", 42) + require.NoError(t, err) + require.Equal(t, resExp, res) + + _, err = inv.Verify(util.Uint160{}, nil, make(map[int]int)) + require.Error(t, err) + + _, err = inv.Call(util.Uint160{}, "method", make(map[int]int)) + require.Error(t, err) + } + t.Run("standard", func(t *testing.T) { + testInv(t, New(ri, nil)) + }) + t.Run("historic, block", func(t *testing.T) { + testInv(t, NewHistoricAtBlock(util.Uint256{}, ri, nil)) + }) + t.Run("historic, height", func(t *testing.T) { + testInv(t, NewHistoricAtHeight(100500, ri, nil)) + }) + t.Run("historic, state", func(t *testing.T) { + testInv(t, NewHistoricWithState(util.Uint256{}, ri, nil)) + }) + t.Run("broken historic", func(t *testing.T) { + inv := New(&historicConverter{client: ri}, nil) // It's not possible to do this from outside. + require.Panics(t, func() { _, _ = inv.Call(util.Uint160{}, "method") }) + require.Panics(t, func() { _, _ = inv.Verify(util.Uint160{}, nil, "param") }) + require.Panics(t, func() { _, _ = inv.Run([]byte{1}) }) + }) +} From b52282c3c77a01654133a4841a7119e58d878c81 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 1 Aug 2022 13:37:07 +0300 Subject: [PATCH 7/8] rpcclient: use Invoker internally for external APIs It's not a big improvement, but it allows to test Invoker better. --- pkg/rpcclient/client.go | 7 +++++++ pkg/rpcclient/native.go | 37 ++++--------------------------------- pkg/rpcclient/nep.go | 19 ++++++------------- pkg/rpcclient/nep11.go | 28 +++++----------------------- pkg/rpcclient/policy.go | 8 ++------ 5 files changed, 24 insertions(+), 75 deletions(-) diff --git a/pkg/rpcclient/client.go b/pkg/rpcclient/client.go index 1b70593fd..6e1fc7dc1 100644 --- a/pkg/rpcclient/client.go +++ b/pkg/rpcclient/client.go @@ -14,6 +14,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config/netmode" "github.com/nspcc-dev/neo-go/pkg/neorpc" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/util" "go.uber.org/atomic" ) @@ -35,6 +36,11 @@ type Client struct { opts Options requestF func(*neorpc.Request) (*neorpc.Response, error) + // reader is an Invoker that has no signers and uses current state, + // it's used to implement various getters. It'll be removed eventually, + // but for now it keeps Client's API compatibility. + reader *invoker.Invoker + cacheLock sync.RWMutex // cache stores RPC node related information the client is bound to. // cache is mostly filled in during Init(), but can also be updated @@ -128,6 +134,7 @@ func initClient(ctx context.Context, cl *Client, endpoint string, opts Options) cl.getNextRequestID = (cl).getRequestID cl.opts = opts cl.requestF = cl.makeHTTPRequest + cl.reader = invoker.New(cl, nil) return nil } diff --git a/pkg/rpcclient/native.go b/pkg/rpcclient/native.go index 4b7d49ccb..2bbb49e7e 100644 --- a/pkg/rpcclient/native.go +++ b/pkg/rpcclient/native.go @@ -5,7 +5,6 @@ package rpcclient import ( "errors" "fmt" - "math/big" "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" @@ -55,16 +54,7 @@ func (c *Client) GetDesignatedByRole(role noderoles.Role, index uint32) (keys.Pu if err != nil { return nil, fmt.Errorf("failed to get native RoleManagement hash: %w", err) } - result, err := c.InvokeFunction(rmHash, "getDesignatedByRole", []smartcontract.Parameter{ - { - Type: smartcontract.IntegerType, - Value: big.NewInt(int64(role)), - }, - { - Type: smartcontract.IntegerType, - Value: big.NewInt(int64(index)), - }, - }, nil) + result, err := c.reader.Call(rmHash, "getDesignatedByRole", int64(role), index) if err != nil { return nil, err } @@ -80,16 +70,7 @@ func (c *Client) NNSResolve(nnsHash util.Uint160, name string, typ nns.RecordTyp if typ == nns.CNAME { return "", errors.New("can't resolve CNAME record type") } - result, err := c.InvokeFunction(nnsHash, "resolve", []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: name, - }, - { - Type: smartcontract.IntegerType, - Value: big.NewInt(int64(typ)), - }, - }, nil) + result, err := c.reader.Call(nnsHash, "resolve", name, int64(typ)) if err != nil { return "", err } @@ -102,12 +83,7 @@ func (c *Client) NNSResolve(nnsHash util.Uint160, name string, typ nns.RecordTyp // NNSIsAvailable invokes `isAvailable` method on a NeoNameService contract with the specified hash. func (c *Client) NNSIsAvailable(nnsHash util.Uint160, name string) (bool, error) { - result, err := c.InvokeFunction(nnsHash, "isAvailable", []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: name, - }, - }, nil) + result, err := c.reader.Call(nnsHash, "isAvailable", name) if err != nil { return false, err } @@ -124,12 +100,7 @@ func (c *Client) NNSIsAvailable(nnsHash util.Uint160, name string) (bool, error) // TerminateSession to terminate opened iterator session. See TraverseIterator and // TerminateSession documentation for more details. func (c *Client) NNSGetAllRecords(nnsHash util.Uint160, name string) (uuid.UUID, result.Iterator, error) { - res, err := c.InvokeFunction(nnsHash, "getAllRecords", []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: name, - }, - }, nil) + res, err := c.reader.Call(nnsHash, "getAllRecords", name) if err != nil { return uuid.UUID{}, result.Iterator{}, err } diff --git a/pkg/rpcclient/nep.go b/pkg/rpcclient/nep.go index e761e0352..7e0a5abbf 100644 --- a/pkg/rpcclient/nep.go +++ b/pkg/rpcclient/nep.go @@ -3,14 +3,13 @@ package rpcclient import ( "fmt" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" ) // nepDecimals invokes `decimals` NEP* method on the specified contract. func (c *Client) nepDecimals(tokenHash util.Uint160) (int64, error) { - result, err := c.InvokeFunction(tokenHash, "decimals", []smartcontract.Parameter{}, nil) + result, err := c.reader.Call(tokenHash, "decimals") if err != nil { return 0, err } @@ -24,7 +23,7 @@ func (c *Client) nepDecimals(tokenHash util.Uint160) (int64, error) { // nepSymbol invokes `symbol` NEP* method on the specified contract. func (c *Client) nepSymbol(tokenHash util.Uint160) (string, error) { - result, err := c.InvokeFunction(tokenHash, "symbol", []smartcontract.Parameter{}, nil) + result, err := c.reader.Call(tokenHash, "symbol") if err != nil { return "", err } @@ -38,7 +37,7 @@ func (c *Client) nepSymbol(tokenHash util.Uint160) (string, error) { // nepTotalSupply invokes `totalSupply` NEP* method on the specified contract. func (c *Client) nepTotalSupply(tokenHash util.Uint160) (int64, error) { - result, err := c.InvokeFunction(tokenHash, "totalSupply", []smartcontract.Parameter{}, nil) + result, err := c.reader.Call(tokenHash, "totalSupply") if err != nil { return 0, err } @@ -52,17 +51,11 @@ func (c *Client) nepTotalSupply(tokenHash util.Uint160) (int64, error) { // nepBalanceOf invokes `balanceOf` NEP* method on the specified contract. func (c *Client) nepBalanceOf(tokenHash, acc util.Uint160, tokenID []byte) (int64, error) { - params := []smartcontract.Parameter{{ - Type: smartcontract.Hash160Type, - Value: acc, - }} + params := []interface{}{acc} if tokenID != nil { - params = append(params, smartcontract.Parameter{ - Type: smartcontract.ByteArrayType, - Value: tokenID, - }) + params = append(params, tokenID) } - result, err := c.InvokeFunction(tokenHash, "balanceOf", params, nil) + result, err := c.reader.Call(tokenHash, "balanceOf", params...) if err != nil { return 0, err } diff --git a/pkg/rpcclient/nep11.go b/pkg/rpcclient/nep11.go index 0877a54ef..f5e908b2b 100644 --- a/pkg/rpcclient/nep11.go +++ b/pkg/rpcclient/nep11.go @@ -84,12 +84,7 @@ func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint1 // traverse iterator values or TerminateSession to terminate opened iterator // session. See TraverseIterator and TerminateSession documentation for more details. func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) (uuid.UUID, result.Iterator, error) { - res, err := c.InvokeFunction(tokenHash, "tokensOf", []smartcontract.Parameter{ - { - Type: smartcontract.Hash160Type, - Value: owner, - }, - }, nil) + res, err := c.reader.Call(tokenHash, "tokensOf", owner) if err != nil { return uuid.UUID{}, result.Iterator{}, err } @@ -136,12 +131,7 @@ func (c *Client) NEP11UnpackedTokensOf(tokenHash util.Uint160, owner util.Uint16 // NEP11NDOwnerOf invokes `ownerOf` non-divisible NEP-11 method with the // specified token ID on the specified contract. func (c *Client) NEP11NDOwnerOf(tokenHash util.Uint160, tokenID []byte) (util.Uint160, error) { - result, err := c.InvokeFunction(tokenHash, "ownerOf", []smartcontract.Parameter{ - { - Type: smartcontract.ByteArrayType, - Value: tokenID, - }, - }, nil) + result, err := c.reader.Call(tokenHash, "ownerOf", tokenID) if err != nil { return util.Uint160{}, err } @@ -186,12 +176,7 @@ func (c *Client) NEP11DBalanceOf(tokenHash, owner util.Uint160, tokenID []byte) // method to traverse iterator values or TerminateSession to terminate opened iterator session. See // TraverseIterator and TerminateSession documentation for more details. func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID []byte) (uuid.UUID, result.Iterator, error) { - res, err := c.InvokeFunction(tokenHash, "ownerOf", []smartcontract.Parameter{ - { - Type: smartcontract.ByteArrayType, - Value: tokenID, - }, - }, nil) + res, err := c.reader.Call(tokenHash, "ownerOf", tokenID) sessID := res.Session if err != nil { return sessID, result.Iterator{}, err @@ -241,10 +226,7 @@ func (c *Client) NEP11DUnpackedOwnerOf(tokenHash util.Uint160, tokenID []byte) ( // NEP11Properties invokes `properties` optional NEP-11 method on the // specified contract. func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID []byte) (*stackitem.Map, error) { - result, err := c.InvokeFunction(tokenHash, "properties", []smartcontract.Parameter{{ - Type: smartcontract.ByteArrayType, - Value: tokenID, - }}, nil) + result, err := c.reader.Call(tokenHash, "properties", tokenID) if err != nil { return nil, err } @@ -262,7 +244,7 @@ func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID []byte) (*stack // TerminateSession to terminate opened iterator session. See TraverseIterator and // TerminateSession documentation for more details. func (c *Client) NEP11Tokens(tokenHash util.Uint160) (uuid.UUID, result.Iterator, error) { - res, err := c.InvokeFunction(tokenHash, "tokens", []smartcontract.Parameter{}, nil) + res, err := c.reader.Call(tokenHash, "tokens") if err != nil { return uuid.UUID{}, result.Iterator{}, err } diff --git a/pkg/rpcclient/policy.go b/pkg/rpcclient/policy.go index 481848526..b2b6372eb 100644 --- a/pkg/rpcclient/policy.go +++ b/pkg/rpcclient/policy.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" ) @@ -42,7 +41,7 @@ func (c *Client) invokeNativePolicyMethod(operation string) (int64, error) { } func (c *Client) invokeNativeGetMethod(hash util.Uint160, operation string) (int64, error) { - result, err := c.InvokeFunction(hash, operation, []smartcontract.Parameter{}, nil) + result, err := c.reader.Call(hash, operation) if err != nil { return 0, err } @@ -59,10 +58,7 @@ func (c *Client) IsBlocked(hash util.Uint160) (bool, error) { if err != nil { return false, fmt.Errorf("failed to get native Policy hash: %w", err) } - result, err := c.InvokeFunction(policyHash, "isBlocked", []smartcontract.Parameter{{ - Type: smartcontract.Hash160Type, - Value: hash, - }}, nil) + result, err := c.reader.Call(policyHash, "isBlocked", hash) if err != nil { return false, err } From 31c9ae6339cee8aa98021655465b9ef393f1d566 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 1 Aug 2022 15:27:36 +0300 Subject: [PATCH 8/8] rpcclient: add CallAndExpandIterator to Invoker And deprecate Client.InvokeAndPackIteratorResults. --- pkg/rpcclient/helper.go | 3 +++ pkg/rpcclient/invoker/invoker.go | 18 ++++++++++++++++++ pkg/rpcclient/invoker/invoker_test.go | 7 +++++++ pkg/rpcclient/native.go | 9 ++------- pkg/rpcclient/nep11.go | 17 ++++------------- pkg/services/rpcsrv/client_test.go | 4 ++-- 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/pkg/rpcclient/helper.go b/pkg/rpcclient/helper.go index 6f91a5855..10656b289 100644 --- a/pkg/rpcclient/helper.go +++ b/pkg/rpcclient/helper.go @@ -111,6 +111,9 @@ func topMapFromStack(st []stackitem.Item) (*stackitem.Map, error) { // retrieve iterator values via single `invokescript` JSON-RPC call. It returns // maxIteratorResultItems items at max which is set to // config.DefaultMaxIteratorResultItems by default. +// +// Deprecated: please use more convenient and powerful invoker.Invoker interface with +// CallAndExpandIterator method. This method will be removed in future versions. func (c *Client) InvokeAndPackIteratorResults(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer, maxIteratorResultItems ...int) (*result.Invoke, error) { max := config.DefaultMaxIteratorResultItems if len(maxIteratorResultItems) != 0 { diff --git a/pkg/rpcclient/invoker/invoker.go b/pkg/rpcclient/invoker/invoker.go index 8336449b4..16a13c972 100644 --- a/pkg/rpcclient/invoker/invoker.go +++ b/pkg/rpcclient/invoker/invoker.go @@ -1,6 +1,8 @@ package invoker import ( + "fmt" + "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" @@ -125,6 +127,22 @@ func (v *Invoker) Call(contract util.Uint160, operation string, params ...interf return v.client.InvokeFunction(contract, operation, ps, v.signers) } +// CallAndExpandIterator creates a script containing a call of the specified method +// of a contract with given parameters (similar to how Call operates). But then this +// script contains additional code that expects that the result of the first call is +// an iterator. This iterator is traversed extracting values from it and adding them +// into an array until maxItems is reached or iterator has no more elements. The +// result of the whole script is an array containing up to maxResultItems elements +// from the iterator returned from the contract's method call. This script is executed +// using regular JSON-API (according to the way Iterator is set up). +func (v *Invoker) CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error) { + bytes, err := smartcontract.CreateCallAndUnwrapIteratorScript(contract, method, maxItems, params...) + if err != nil { + return nil, fmt.Errorf("iterator unwrapper script: %w", err) + } + return v.Run(bytes) +} + // Verify invokes contract's verify method in the verification context with // Invoker-specific signers and given witnesses and parameters. func (v *Invoker) Verify(contract util.Uint160, witnesses []transaction.Witness, params ...interface{}) (*result.Invoke, error) { diff --git a/pkg/rpcclient/invoker/invoker_test.go b/pkg/rpcclient/invoker/invoker_test.go index fd755e421..a5e59af3e 100644 --- a/pkg/rpcclient/invoker/invoker_test.go +++ b/pkg/rpcclient/invoker/invoker_test.go @@ -86,6 +86,13 @@ func TestInvoker(t *testing.T) { _, err = inv.Call(util.Uint160{}, "method", make(map[int]int)) require.Error(t, err) + + res, err = inv.CallAndExpandIterator(util.Uint160{}, "method", 10, 42) + require.NoError(t, err) + require.Equal(t, resExp, res) + + _, err = inv.CallAndExpandIterator(util.Uint160{}, "method", 10, make(map[int]int)) + require.Error(t, err) } t.Run("standard", func(t *testing.T) { testInv(t, New(ri, nil)) diff --git a/pkg/rpcclient/native.go b/pkg/rpcclient/native.go index 2bbb49e7e..8d1741f43 100644 --- a/pkg/rpcclient/native.go +++ b/pkg/rpcclient/native.go @@ -7,12 +7,12 @@ import ( "fmt" "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/native/nativenames" "github.com/nspcc-dev/neo-go/pkg/core/native/noderoles" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nns" - "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" ) @@ -118,12 +118,7 @@ func (c *Client) NNSGetAllRecords(nnsHash util.Uint160, name string) (uuid.UUID, // that no iterator session is used to retrieve values from iterator. Instead, unpacking // VM script is created and invoked via `invokescript` JSON-RPC call. func (c *Client) NNSUnpackedGetAllRecords(nnsHash util.Uint160, name string) ([]nns.RecordState, error) { - result, err := c.InvokeAndPackIteratorResults(nnsHash, "getAllRecords", []smartcontract.Parameter{ - { - Type: smartcontract.StringType, - Value: name, - }, - }, nil) + result, err := c.reader.CallAndExpandIterator(nnsHash, "getAllRecords", config.DefaultMaxIteratorResultItems, name) if err != nil { return nil, err } diff --git a/pkg/rpcclient/nep11.go b/pkg/rpcclient/nep11.go index f5e908b2b..4865ea78f 100644 --- a/pkg/rpcclient/nep11.go +++ b/pkg/rpcclient/nep11.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/google/uuid" + "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/neorpc/result" @@ -101,12 +102,7 @@ func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) (uuid // is used to retrieve values from iterator. Instead, unpacking VM script is created and invoked via // `invokescript` JSON-RPC call. func (c *Client) NEP11UnpackedTokensOf(tokenHash util.Uint160, owner util.Uint160) ([][]byte, error) { - result, err := c.InvokeAndPackIteratorResults(tokenHash, "tokensOf", []smartcontract.Parameter{ - { - Type: smartcontract.Hash160Type, - Value: owner, - }, - }, nil) + result, err := c.reader.CallAndExpandIterator(tokenHash, "tokensOf", config.DefaultMaxIteratorResultItems, owner) if err != nil { return nil, err } @@ -194,12 +190,7 @@ func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID []byte) (uuid.UUI // iterator session is used to retrieve values from iterator. Instead, unpacking VM // script is created and invoked via `invokescript` JSON-RPC call. func (c *Client) NEP11DUnpackedOwnerOf(tokenHash util.Uint160, tokenID []byte) ([]util.Uint160, error) { - result, err := c.InvokeAndPackIteratorResults(tokenHash, "ownerOf", []smartcontract.Parameter{ - { - Type: smartcontract.ByteArrayType, - Value: tokenID, - }, - }, nil) + result, err := c.reader.CallAndExpandIterator(tokenHash, "ownerOf", config.DefaultMaxIteratorResultItems, tokenID) if err != nil { return nil, err } @@ -261,7 +252,7 @@ func (c *Client) NEP11Tokens(tokenHash util.Uint160) (uuid.UUID, result.Iterator // iterator session is used to retrieve values from iterator. Instead, unpacking // VM script is created and invoked via `invokescript` JSON-RPC call. func (c *Client) NEP11UnpackedTokens(tokenHash util.Uint160) ([][]byte, error) { - result, err := c.InvokeAndPackIteratorResults(tokenHash, "tokens", []smartcontract.Parameter{}, nil) + result, err := c.reader.CallAndExpandIterator(tokenHash, "tokens", config.DefaultMaxIteratorResultItems) if err != nil { return nil, err } diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index 4c12cf9d1..a53ce077f 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -1280,7 +1280,7 @@ func TestClient_InvokeAndPackIteratorResults(t *testing.T) { require.NoError(t, err) t.Run("default max items constraint", func(t *testing.T) { - res, err := c.InvokeAndPackIteratorResults(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil) + res, err := c.InvokeAndPackIteratorResults(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil) //nolint:staticcheck // SA1019: c.InvokeAndPackIteratorResults is deprecated require.NoError(t, err) require.Equal(t, vmstate.Halt.String(), res.State) require.Equal(t, 1, len(res.Stack)) @@ -1296,7 +1296,7 @@ func TestClient_InvokeAndPackIteratorResults(t *testing.T) { }) t.Run("custom max items constraint", func(t *testing.T) { max := 123 - res, err := c.InvokeAndPackIteratorResults(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil, max) + res, err := c.InvokeAndPackIteratorResults(storageHash, "iterateOverValues", []smartcontract.Parameter{}, nil, max) //nolint:staticcheck // SA1019: c.InvokeAndPackIteratorResults is deprecated require.NoError(t, err) require.Equal(t, vmstate.Halt.String(), res.State) require.Equal(t, 1, len(res.Stack))