Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 96 additions & 5 deletions jsonrpc/params.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package jsonrpc

import (
"bytes"
"encoding/json"

"fmt"
"github.com/pkg/errors"
"io"
"reflect"
"strings"
)

// Params is an ARRAY of json.RawMessages. This is because *Ethereum* RPCs always use
Expand Down Expand Up @@ -74,7 +78,8 @@ func MakeParams(params ...interface{}) (Params, error) {
// UnmarshalInto will decode Params into the passed in values, which
// must be pointer receivers. The type of the passed in value is used to Unmarshal the data.
// UnmarshalInto will fail if the parameters cannot be converted to the passed-in types.
//
// Check each type of each param, return an error if it's not the right one and which argument.

// Example:
//
// var blockNum string
Expand All @@ -83,6 +88,7 @@ func MakeParams(params ...interface{}) (Params, error) {
//
// IMPORTANT: While Go will compile with non-pointer receivers, the Unmarshal attempt will
// *always* fail with an error.

func (p Params) UnmarshalInto(receivers ...interface{}) error {
if p == nil {
return nil
Expand All @@ -92,11 +98,30 @@ func (p Params) UnmarshalInto(receivers ...interface{}) error {
return errors.New("not enough params to decode")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this check required anymore? if you pass more receivers than params, isn't it enough to set them to nil? That seems to be the intended behavior copied across from go-ethereum library at line 169

Copy link
Copy Markdown
Contributor Author

@Caroline-theotter Caroline-theotter Oct 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure to understand @ggarri

}

// Return an array of the receivers' types and check if the receiver is a ptr
receiversType, err := listTypes(receivers)
if err != nil {
return err
}

var paramElement []string
for _, i := range p {
paramElement = append(paramElement, string(i))
}

// Return p Params in json.RawMessage type with [] to be parsed
rawParams := json.RawMessage("[" + strings.Join(paramElement, ",") + "]")

receiversValues, err := parsePositionalArguments(rawParams, receiversType)
if err != nil {
return err
}

for i, r := range receivers {
Comment thread
Caroline-theotter marked this conversation as resolved.
err := json.Unmarshal(p[i], r)
if err != nil {
return err
if receiversValues[i].IsZero() {
continue
}
reflect.ValueOf(r).Elem().Set(receiversValues[i].Elem())
}

return nil
Expand All @@ -117,3 +142,69 @@ func (p Params) UnmarshalSingleParam(pos int, receiver interface{}) error {
err := json.Unmarshal(param, receiver)
return err
}

// parsePositionalArguments tries to parse the given args to an array of values with the
// given types. It returns the parsed values or an error when the args could not be
// parsed. Missing optional arguments are returned as reflect.Zero values.
func parsePositionalArguments(rawArgs json.RawMessage, types []reflect.Type) ([]reflect.Value, error) {
dec := json.NewDecoder(bytes.NewReader(rawArgs))
var args []reflect.Value
tok, err := dec.Token()
switch {
case err == io.EOF || tok == nil && err == nil:
// "params" is optional and may be empty. Also allow "params":null even though it's
// not in the spec because our own client used to send it.
case err != nil:
return nil, err
case tok == json.Delim('['):
// Read argument array.
if args, err = parseArgumentArray(dec, types); err != nil {
return nil, err
}
default:
return nil, errors.New("non-array args")
}
// Set any missing args to nil.
for i := len(args); i < len(types); i++ {
if types[i].Kind() != reflect.Ptr {
return nil, fmt.Errorf("missing value for required argument %d", i)
}
args = append(args, reflect.Zero(types[i]))
}
return args, nil
}

func parseArgumentArray(dec *json.Decoder, types []reflect.Type) ([]reflect.Value, error) {
args := make([]reflect.Value, 0, len(types))

for i := 0; dec.More(); i++ {
if i >= len(types) { //no error when decoding a subset of param
return args, nil
}
argval := reflect.New(types[i])

if err := dec.Decode(argval.Interface()); err != nil {
return args, fmt.Errorf("invalid argument %d: %v", i, err)
}

if argval.IsNil() && types[i].Kind() != reflect.Ptr {
return args, fmt.Errorf("missing value for required argument %d", i)
}
args = append(args, argval.Elem())
}
// Read end of args array.
_, err := dec.Token()
return args, err
}

func listTypes(a []interface{}) ([]reflect.Type, error) {
var arrayType []reflect.Type
for _, i := range a {
v := reflect.ValueOf(i).Type()
Comment thread
Caroline-theotter marked this conversation as resolved.
if v.Kind() != reflect.Ptr {
return nil, fmt.Errorf("the receiver %d is not a pointer", i)
}
arrayType = append(arrayType, v)
}
return arrayType, nil
}
199 changes: 199 additions & 0 deletions jsonrpc/params_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package jsonrpc

import (
"bytes"
"encoding/json"
"fmt"
"github.com/INFURA/go-ethlibs/eth"
"github.com/pkg/errors"
"reflect"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -107,6 +113,27 @@ func TestParams_DecodeInto(t *testing.T) {
return []interface{}{str}, err
},
},
{
Description: "receiver's type is a struct",
Expected: []interface{}{eth.LogFilter{FromBlock: eth.MustBlockNumberOrTag("0x3456789"), ToBlock: eth.MustBlockNumberOrTag("0x3456"), BlockHash: (*eth.Data32)(nil), Address: []eth.Address(nil), Topics: [][]eth.Data32(nil)}},
Input: MustParams(&eth.LogFilter{FromBlock: eth.MustBlockNumberOrTag("0x3456789"), ToBlock: eth.MustBlockNumberOrTag("0x3456")}),
Test: func(tc *testCase) ([]interface{}, error) {
var rec eth.LogFilter
err := tc.Input.UnmarshalInto(&rec)
return []interface{}{rec}, err
},
},
{
Description: "multiple element in params",
Expected: []interface{}{eth.LogFilter{FromBlock: eth.MustBlockNumberOrTag("0x3456789"), ToBlock: eth.MustBlockNumberOrTag("0x3456")}, eth.LogFilter{FromBlock: eth.MustBlockNumberOrTag("0x5678"), ToBlock: eth.MustBlockNumberOrTag("0x1234")}},
Input: MustParams(&eth.LogFilter{FromBlock: eth.MustBlockNumberOrTag("0x3456789"), ToBlock: eth.MustBlockNumberOrTag("0x3456")}, &eth.LogFilter{FromBlock: eth.MustBlockNumberOrTag("0x5678"), ToBlock: eth.MustBlockNumberOrTag("0x1234")}),
Test: func(tc *testCase) ([]interface{}, error) {
var rec1 eth.LogFilter
var rec2 eth.LogFilter
err := tc.Input.UnmarshalInto(&rec1, &rec2)
return []interface{}{rec1, rec2}, err
},
},
}

for _, testCase := range testCases {
Expand Down Expand Up @@ -134,3 +161,175 @@ func TestParams_DecodeInto(t *testing.T) {
object := Object{}
assert.Error(t, multiple.UnmarshalSingleParam(3, &object), "should have failed")
}

func TestParams_DecodeInto_Fail(t *testing.T) {

type expected struct {
output []interface{}
err error
}
type testCase struct {
Description string
Expected expected
Input Params
Test func(tc *testCase) ([]interface{}, error)
}

testCases := []testCase{
{
Description: "params null",
Expected: expected{output: nil, err: nil},
Input: nil,
Test: func(tc *testCase) ([]interface{}, error) {
var str string
err := tc.Input.UnmarshalInto(str)
return nil, err
},
},
{
Description: "len(p)<len(rec)",
Expected: expected{output: []interface{}{}, err: errors.New("not enough params to decode")},
Input: MustParams("foo"),
Test: func(tc *testCase) ([]interface{}, error) {
var str string
var b bool
err := tc.Input.UnmarshalInto(&str, &b)
return []interface{}{}, err
},
},
{
Description: "parse err",
Expected: expected{output: []interface{}{}, err: errors.New("invalid argument 0: data types must start with 0x")},
Input: MustParams("2345T678"),
Test: func(tc *testCase) ([]interface{}, error) {
var str eth.Hash
err := tc.Input.UnmarshalInto(&str)
return []interface{}{}, err
},
},
}

for _, testCase := range testCases {
actual, err := testCase.Test(&testCase)

assert.Equal(t, testCase.Expected.output, actual, "%#v", testCase)
if err != nil {
assert.Equal(t, testCase.Expected.err.Error(), err.Error(), "%#v", testCase)
} else {
assert.Nil(t, testCase.Expected.err, "%#v", testCase)
}
}

}

func TestParams_parsePositionalArguments(t *testing.T) {
type expected struct {
args []reflect.Value
err error
}

type testCase struct {
Description string
Expected expected
rawArgs json.RawMessage
types []reflect.Type
}

testCases := []testCase{
{
Description: "default case err",
Expected: expected{args: []reflect.Value(nil), err: errors.New("non-array args")},
rawArgs: []byte(`{"foo"}`),
types: []reflect.Type{},
},
{
Description: "params nil",
Expected: expected{args: nil, err: nil},
rawArgs: []byte(nil),
types: []reflect.Type{},
},
{
Description: "token err",
Expected: expected{args: nil, err: errors.New("invalid character ',' looking for beginning of value")},
rawArgs: []byte(","),
types: []reflect.Type{},
},
{
Description: "reading err",
Expected: expected{args: nil, err: fmt.Errorf("EOF")},
rawArgs: []byte("["),
types: []reflect.Type{},
},
{
Description: "missing value for arg",
Expected: expected{args: nil, err: fmt.Errorf("missing value for required argument 0")},
rawArgs: []byte(nil),
types: []reflect.Type{reflect.TypeOf("foo"), reflect.TypeOf(true)},
},
{
Description: "works",
Expected: expected{args: []reflect.Value{reflect.ValueOf("foo")}, err: nil},
rawArgs: []byte(`["foo"]`),
types: []reflect.Type{reflect.TypeOf("foo")},
},
}

for _, testCase := range testCases {

actual, err := parsePositionalArguments(testCase.rawArgs, testCase.types)
assert.ObjectsAreEqualValues(testCase.Expected.args, actual)
if err != nil {
assert.Equal(t, testCase.Expected.err.Error(), err.Error(), "%#v", testCase)
} else {
assert.Nil(t, testCase.Expected.err, "%#v", testCase)
}
}
}

func TestParams_parseArgumentArray(t *testing.T) {
type expected struct {
args []reflect.Value
err error
}

type testCase struct {
Description string
Expected expected
dec *json.Decoder
types []reflect.Type
}

testCases := []testCase{
{
Description: "decode subset of param",
Expected: expected{args: []reflect.Value{reflect.ValueOf("foo")}, err: nil},
dec: json.NewDecoder(bytes.NewReader([]byte(`["foo", 123]`))),
types: []reflect.Type{reflect.TypeOf("foo")},
},
{
Description: "works",
Expected: expected{args: []reflect.Value{reflect.ValueOf("foo")}, err: nil},
dec: json.NewDecoder(bytes.NewReader([]byte(`["foo"]`))),
types: []reflect.Type{reflect.TypeOf("foo")},
},

{
Description: "EOF",
Expected: expected{args: []reflect.Value{reflect.ValueOf(nil)}, err: fmt.Errorf("EOF")},
dec: json.NewDecoder(bytes.NewReader([]byte(``))),
types: []reflect.Type{reflect.TypeOf("foo")},
},
}

for _, testCase := range testCases {
_, _ = testCase.dec.Token()
actual, err := parseArgumentArray(testCase.dec, testCase.types)
assert.ObjectsAreEqualValues(testCase.Expected.args, actual)
if err != nil {
assert.Equal(t, testCase.Expected.err, err, "%#v", testCase)
} else {
assert.Nil(t, testCase.Expected.err, "%#v", testCase)
}
}

}