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/helper.go b/pkg/rpcclient/helper.go index 3db0052d3..10656b289 100644 --- a/pkg/rpcclient/helper.go +++ b/pkg/rpcclient/helper.go @@ -111,12 +111,22 @@ 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 { 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/rpcclient/invoker/invoker.go b/pkg/rpcclient/invoker/invoker.go new file mode 100644 index 000000000..16a13c972 --- /dev/null +++ b/pkg/rpcclient/invoker/invoker.go @@ -0,0 +1,159 @@ +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" + "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) +} + +// 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) { + 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..a5e59af3e --- /dev/null +++ b/pkg/rpcclient/invoker/invoker_test.go @@ -0,0 +1,115 @@ +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) + + 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)) + }) + 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}) }) + }) +} diff --git a/pkg/rpcclient/native.go b/pkg/rpcclient/native.go index 4b7d49ccb..8d1741f43 100644 --- a/pkg/rpcclient/native.go +++ b/pkg/rpcclient/native.go @@ -5,15 +5,14 @@ package rpcclient import ( "errors" "fmt" - "math/big" "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" ) @@ -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 } @@ -147,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/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..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" @@ -84,12 +85,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 } @@ -106,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 } @@ -136,12 +127,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 +172,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 @@ -209,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 } @@ -241,10 +217,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 +235,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 } @@ -279,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/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 } 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..a53ce077f 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)) @@ -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)) 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 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() diff --git a/pkg/smartcontract/parameter.go b/pkg/smartcontract/parameter.go index 97cb04a2c..e4496fd20 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 @@ -375,6 +255,111 @@ 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, err := NewParametersFromValues(v...) + if err != nil { + return result, err + } + result.Type = ArrayType + result.Value = arr + default: + return result, fmt.Errorf("unsupported parameter %T", value) + } + + 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 e52cc67a1..181901876 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, @@ -611,3 +527,185 @@ 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) +} + +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) +}