aboutsummaryrefslogtreecommitdiffstats
path: root/accounts
diff options
context:
space:
mode:
authorMartin Holst Swende <martin@swende.se>2017-12-23 03:59:41 +0800
committerGitHub <noreply@github.com>2017-12-23 03:59:41 +0800
commit9d187f02389ba12493112c7feb15a83f44e3a3ff (patch)
treec98e69e41f2412d8ccf6ebec886ed2110be866bb /accounts
parent5f8888e11606296c9582496974c0f6b96a882146 (diff)
parentc095c87e117785ba5467487336215f86a958409b (diff)
downloaddexon-9d187f02389ba12493112c7feb15a83f44e3a3ff.tar
dexon-9d187f02389ba12493112c7feb15a83f44e3a3ff.tar.gz
dexon-9d187f02389ba12493112c7feb15a83f44e3a3ff.tar.bz2
dexon-9d187f02389ba12493112c7feb15a83f44e3a3ff.tar.lz
dexon-9d187f02389ba12493112c7feb15a83f44e3a3ff.tar.xz
dexon-9d187f02389ba12493112c7feb15a83f44e3a3ff.tar.zst
dexon-9d187f02389ba12493112c7feb15a83f44e3a3ff.zip
Merge pull request #15731 from holiman/revamp_abi
accounts/abi refactor
Diffstat (limited to 'accounts')
-rw-r--r--accounts/abi/abi.go52
-rw-r--r--accounts/abi/abi_test.go63
-rw-r--r--accounts/abi/argument.go181
-rw-r--r--accounts/abi/event.go95
-rw-r--r--accounts/abi/event_test.go237
-rw-r--r--accounts/abi/method.go162
-rw-r--r--accounts/abi/pack.go3
-rw-r--r--accounts/abi/reflect.go25
-rw-r--r--accounts/abi/type.go114
-rw-r--r--accounts/abi/unpack.go13
-rw-r--r--accounts/abi/unpack_test.go100
11 files changed, 666 insertions, 379 deletions
diff --git a/accounts/abi/abi.go b/accounts/abi/abi.go
index 02b4fa472..cbcf4ca92 100644
--- a/accounts/abi/abi.go
+++ b/accounts/abi/abi.go
@@ -17,6 +17,7 @@
package abi
import (
+ "bytes"
"encoding/json"
"fmt"
"io"
@@ -50,25 +51,25 @@ func JSON(reader io.Reader) (ABI, error) {
// methods string signature. (signature = baz(uint32,string32))
func (abi ABI) Pack(name string, args ...interface{}) ([]byte, error) {
// Fetch the ABI of the requested method
- var method Method
-
if name == "" {
- method = abi.Constructor
- } else {
- m, exist := abi.Methods[name]
- if !exist {
- return nil, fmt.Errorf("method '%s' not found", name)
+ // constructor
+ arguments, err := abi.Constructor.Inputs.Pack(args...)
+ if err != nil {
+ return nil, err
}
- method = m
+ return arguments, nil
+
+ }
+ method, exist := abi.Methods[name]
+ if !exist {
+ return nil, fmt.Errorf("method '%s' not found", name)
}
- arguments, err := method.pack(args...)
+
+ arguments, err := method.Inputs.Pack(args...)
if err != nil {
return nil, err
}
// Pack up the method ID too if not a constructor and return
- if name == "" {
- return arguments, nil
- }
return append(method.Id(), arguments...), nil
}
@@ -77,28 +78,20 @@ func (abi ABI) Unpack(v interface{}, name string, output []byte) (err error) {
if len(output) == 0 {
return fmt.Errorf("abi: unmarshalling empty output")
}
-
// since there can't be naming collisions with contracts and events,
// we need to decide whether we're calling a method or an event
- var unpack unpacker
if method, ok := abi.Methods[name]; ok {
if len(output)%32 != 0 {
return fmt.Errorf("abi: improperly formatted output")
}
- unpack = method
+ return method.Outputs.Unpack(v, output)
} else if event, ok := abi.Events[name]; ok {
- unpack = event
- } else {
- return fmt.Errorf("abi: could not locate named method or event.")
- }
-
- // requires a struct to unpack into for a tuple return...
- if unpack.isTupleReturn() {
- return unpack.tupleUnpack(v, output)
+ return event.Inputs.Unpack(v, output)
}
- return unpack.singleUnpack(v, output)
+ return fmt.Errorf("abi: could not locate named method or event")
}
+// UnmarshalJSON implements json.Unmarshaler interface
func (abi *ABI) UnmarshalJSON(data []byte) error {
var fields []struct {
Type string
@@ -141,3 +134,14 @@ func (abi *ABI) UnmarshalJSON(data []byte) error {
return nil
}
+
+// MethodById looks up a method by the 4-byte id
+// returns nil if none found
+func (abi *ABI) MethodById(sigdata []byte) *Method {
+ for _, method := range abi.Methods {
+ if bytes.Equal(method.Id(), sigdata[:4]) {
+ return &method
+ }
+ }
+ return nil
+}
diff --git a/accounts/abi/abi_test.go b/accounts/abi/abi_test.go
index 644a388e3..2d43b631c 100644
--- a/accounts/abi/abi_test.go
+++ b/accounts/abi/abi_test.go
@@ -22,10 +22,11 @@ import (
"fmt"
"log"
"math/big"
- "reflect"
"strings"
"testing"
+ "reflect"
+
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
)
@@ -75,9 +76,24 @@ func TestReader(t *testing.T) {
}
// deep equal fails for some reason
- t.Skip()
- if !reflect.DeepEqual(abi, exp) {
- t.Errorf("\nabi: %v\ndoes not match exp: %v", abi, exp)
+ for name, expM := range exp.Methods {
+ gotM, exist := abi.Methods[name]
+ if !exist {
+ t.Errorf("Missing expected method %v", name)
+ }
+ if !reflect.DeepEqual(gotM, expM) {
+ t.Errorf("\nGot abi method: \n%v\ndoes not match expected method\n%v", gotM, expM)
+ }
+ }
+
+ for name, gotM := range abi.Methods {
+ expM, exist := exp.Methods[name]
+ if !exist {
+ t.Errorf("Found extra method %v", name)
+ }
+ if !reflect.DeepEqual(gotM, expM) {
+ t.Errorf("\nGot abi method: \n%v\ndoes not match expected method\n%v", gotM, expM)
+ }
}
}
@@ -641,3 +657,42 @@ func TestUnpackEvent(t *testing.T) {
t.Logf("len(data): %d; received event: %+v", len(data), ev)
}
}
+
+func TestABI_MethodById(t *testing.T) {
+ const abiJSON = `[
+ {"type":"function","name":"receive","constant":false,"inputs":[{"name":"memo","type":"bytes"}],"outputs":[],"payable":true,"stateMutability":"payable"},
+ {"type":"event","name":"received","anonymous":false,"inputs":[{"indexed":false,"name":"sender","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"memo","type":"bytes"}]},
+ {"type":"function","name":"fixedArrStr","constant":true,"inputs":[{"name":"str","type":"string"},{"name":"fixedArr","type":"uint256[2]"}]},
+ {"type":"function","name":"fixedArrBytes","constant":true,"inputs":[{"name":"str","type":"bytes"},{"name":"fixedArr","type":"uint256[2]"}]},
+ {"type":"function","name":"mixedArrStr","constant":true,"inputs":[{"name":"str","type":"string"},{"name":"fixedArr","type":"uint256[2]"},{"name":"dynArr","type":"uint256[]"}]},
+ {"type":"function","name":"doubleFixedArrStr","constant":true,"inputs":[{"name":"str","type":"string"},{"name":"fixedArr1","type":"uint256[2]"},{"name":"fixedArr2","type":"uint256[3]"}]},
+ {"type":"function","name":"multipleMixedArrStr","constant":true,"inputs":[{"name":"str","type":"string"},{"name":"fixedArr1","type":"uint256[2]"},{"name":"dynArr","type":"uint256[]"},{"name":"fixedArr2","type":"uint256[3]"}]},
+ {"type":"function","name":"balance","constant":true},
+ {"type":"function","name":"send","constant":false,"inputs":[{"name":"amount","type":"uint256"}]},
+ {"type":"function","name":"test","constant":false,"inputs":[{"name":"number","type":"uint32"}]},
+ {"type":"function","name":"string","constant":false,"inputs":[{"name":"inputs","type":"string"}]},
+ {"type":"function","name":"bool","constant":false,"inputs":[{"name":"inputs","type":"bool"}]},
+ {"type":"function","name":"address","constant":false,"inputs":[{"name":"inputs","type":"address"}]},
+ {"type":"function","name":"uint64[2]","constant":false,"inputs":[{"name":"inputs","type":"uint64[2]"}]},
+ {"type":"function","name":"uint64[]","constant":false,"inputs":[{"name":"inputs","type":"uint64[]"}]},
+ {"type":"function","name":"foo","constant":false,"inputs":[{"name":"inputs","type":"uint32"}]},
+ {"type":"function","name":"bar","constant":false,"inputs":[{"name":"inputs","type":"uint32"},{"name":"string","type":"uint16"}]},
+ {"type":"function","name":"_slice","constant":false,"inputs":[{"name":"inputs","type":"uint32[2]"}]},
+ {"type":"function","name":"__slice256","constant":false,"inputs":[{"name":"inputs","type":"uint256[2]"}]},
+ {"type":"function","name":"sliceAddress","constant":false,"inputs":[{"name":"inputs","type":"address[]"}]},
+ {"type":"function","name":"sliceMultiAddress","constant":false,"inputs":[{"name":"a","type":"address[]"},{"name":"b","type":"address[]"}]}
+ ]
+`
+ abi, err := JSON(strings.NewReader(abiJSON))
+ if err != nil {
+ t.Fatal(err)
+ }
+ for name, m := range abi.Methods {
+ a := fmt.Sprintf("%v", m)
+ b := fmt.Sprintf("%v", abi.MethodById(m.Id()))
+ if a != b {
+ t.Errorf("Method %v (id %v) not 'findable' by id in ABI", name, common.ToHex(m.Id()))
+ }
+ }
+
+}
diff --git a/accounts/abi/argument.go b/accounts/abi/argument.go
index 4691318ce..ad17fbf2b 100644
--- a/accounts/abi/argument.go
+++ b/accounts/abi/argument.go
@@ -19,6 +19,8 @@ package abi
import (
"encoding/json"
"fmt"
+ "reflect"
+ "strings"
)
// Argument holds the name of the argument and the corresponding type.
@@ -29,7 +31,10 @@ type Argument struct {
Indexed bool // indexed is only used by events
}
-func (a *Argument) UnmarshalJSON(data []byte) error {
+type Arguments []Argument
+
+// UnmarshalJSON implements json.Unmarshaler interface
+func (argument *Argument) UnmarshalJSON(data []byte) error {
var extarg struct {
Name string
Type string
@@ -40,12 +45,180 @@ func (a *Argument) UnmarshalJSON(data []byte) error {
return fmt.Errorf("argument json err: %v", err)
}
- a.Type, err = NewType(extarg.Type)
+ argument.Type, err = NewType(extarg.Type)
if err != nil {
return err
}
- a.Name = extarg.Name
- a.Indexed = extarg.Indexed
+ argument.Name = extarg.Name
+ argument.Indexed = extarg.Indexed
return nil
}
+
+// LengthNonIndexed returns the number of arguments when not counting 'indexed' ones. Only events
+// can ever have 'indexed' arguments, it should always be false on arguments for method input/output
+func (arguments Arguments) LengthNonIndexed() int {
+ out := 0
+ for _, arg := range arguments {
+ if !arg.Indexed {
+ out++
+ }
+ }
+ return out
+}
+
+// isTuple returns true for non-atomic constructs, like (uint,uint) or uint[]
+func (arguments Arguments) isTuple() bool {
+ return len(arguments) > 1
+}
+
+// Unpack performs the operation hexdata -> Go format
+func (arguments Arguments) Unpack(v interface{}, data []byte) error {
+ if arguments.isTuple() {
+ return arguments.unpackTuple(v, data)
+ }
+ return arguments.unpackAtomic(v, data)
+}
+
+func (arguments Arguments) unpackTuple(v interface{}, output []byte) error {
+ // make sure the passed value is arguments pointer
+ valueOf := reflect.ValueOf(v)
+ if reflect.Ptr != valueOf.Kind() {
+ return fmt.Errorf("abi: Unpack(non-pointer %T)", v)
+ }
+
+ var (
+ value = valueOf.Elem()
+ typ = value.Type()
+ kind = value.Kind()
+ )
+
+ if err := requireUnpackKind(value, typ, kind, arguments); err != nil {
+ return err
+ }
+ // `i` counts the nonindexed arguments.
+ // `j` counts the number of complex types.
+ // both `i` and `j` are used to to correctly compute `data` offset.
+
+ i, j := -1, 0
+ for _, arg := range arguments {
+
+ if arg.Indexed {
+ // can't read, continue
+ continue
+ }
+ i++
+ marshalledValue, err := toGoType((i+j)*32, arg.Type, output)
+ if err != nil {
+ return err
+ }
+
+ if arg.Type.T == ArrayTy {
+ // combined index ('i' + 'j') need to be adjusted only by size of array, thus
+ // we need to decrement 'j' because 'i' was incremented
+ j += arg.Type.Size - 1
+ }
+
+ reflectValue := reflect.ValueOf(marshalledValue)
+
+ switch kind {
+ case reflect.Struct:
+ for j := 0; j < typ.NumField(); j++ {
+ field := typ.Field(j)
+ // TODO read tags: `abi:"fieldName"`
+ if field.Name == strings.ToUpper(arg.Name[:1])+arg.Name[1:] {
+ if err := set(value.Field(j), reflectValue, arg); err != nil {
+ return err
+ }
+ }
+ }
+ case reflect.Slice, reflect.Array:
+ if value.Len() < i {
+ return fmt.Errorf("abi: insufficient number of arguments for unpack, want %d, got %d", len(arguments), value.Len())
+ }
+ v := value.Index(i)
+ if err := requireAssignable(v, reflectValue); err != nil {
+ return err
+ }
+
+ if err := set(v.Elem(), reflectValue, arg); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("abi:[2] cannot unmarshal tuple in to %v", typ)
+ }
+ }
+ return nil
+}
+
+// unpackAtomic unpacks ( hexdata -> go ) a single value
+func (arguments Arguments) unpackAtomic(v interface{}, output []byte) error {
+ // make sure the passed value is arguments pointer
+ valueOf := reflect.ValueOf(v)
+ if reflect.Ptr != valueOf.Kind() {
+ return fmt.Errorf("abi: Unpack(non-pointer %T)", v)
+ }
+ arg := arguments[0]
+ if arg.Indexed {
+ return fmt.Errorf("abi: attempting to unpack indexed variable into element.")
+ }
+
+ value := valueOf.Elem()
+
+ marshalledValue, err := toGoType(0, arg.Type, output)
+ if err != nil {
+ return err
+ }
+ return set(value, reflect.ValueOf(marshalledValue), arg)
+}
+
+// Unpack performs the operation Go format -> Hexdata
+func (arguments Arguments) Pack(args ...interface{}) ([]byte, error) {
+ // Make sure arguments match up and pack them
+ abiArgs := arguments
+ if len(args) != len(abiArgs) {
+ return nil, fmt.Errorf("argument count mismatch: %d for %d", len(args), len(abiArgs))
+ }
+
+ // variable input is the output appended at the end of packed
+ // output. This is used for strings and bytes types input.
+ var variableInput []byte
+
+ // input offset is the bytes offset for packed output
+ inputOffset := 0
+ for _, abiArg := range abiArgs {
+ if abiArg.Type.T == ArrayTy {
+ inputOffset += (32 * abiArg.Type.Size)
+ } else {
+ inputOffset += 32
+ }
+ }
+
+ var ret []byte
+ for i, a := range args {
+ input := abiArgs[i]
+ // pack the input
+ packed, err := input.Type.pack(reflect.ValueOf(a))
+ if err != nil {
+ return nil, err
+ }
+
+ // check for a slice type (string, bytes, slice)
+ if input.Type.requiresLengthPrefix() {
+ // calculate the offset
+ offset := inputOffset + len(variableInput)
+ // set the offset
+ ret = append(ret, packNum(reflect.ValueOf(offset))...)
+ // Append the packed output to the variable input. The variable input
+ // will be appended at the end of the input.
+ variableInput = append(variableInput, packed...)
+ } else {
+ // append the packed value to the input
+ ret = append(ret, packed...)
+ }
+ }
+ // append the variable input at the end of the packed input
+ ret = append(ret, variableInput...)
+
+ return ret, nil
+}
diff --git a/accounts/abi/event.go b/accounts/abi/event.go
index bd1098d87..726bac90e 100644
--- a/accounts/abi/event.go
+++ b/accounts/abi/event.go
@@ -18,7 +18,6 @@ package abi
import (
"fmt"
- "reflect"
"strings"
"github.com/ethereum/go-ethereum/common"
@@ -31,7 +30,7 @@ import (
type Event struct {
Name string
Anonymous bool
- Inputs []Argument
+ Inputs Arguments
}
// Id returns the canonical representation of the event's signature used by the
@@ -45,95 +44,3 @@ func (e Event) Id() common.Hash {
}
return common.BytesToHash(crypto.Keccak256([]byte(fmt.Sprintf("%v(%v)", e.Name, strings.Join(types, ",")))))
}
-
-// unpacks an event return tuple into a struct of corresponding go types
-//
-// Unpacking can be done into a struct or a slice/array.
-func (e Event) tupleUnpack(v interface{}, output []byte) error {
- // make sure the passed value is a pointer
- valueOf := reflect.ValueOf(v)
- if reflect.Ptr != valueOf.Kind() {
- return fmt.Errorf("abi: Unpack(non-pointer %T)", v)
- }
-
- var (
- value = valueOf.Elem()
- typ = value.Type()
- )
-
- if value.Kind() != reflect.Struct {
- return fmt.Errorf("abi: cannot unmarshal tuple in to %v", typ)
- }
-
- j := 0
- for i := 0; i < len(e.Inputs); i++ {
- input := e.Inputs[i]
- if input.Indexed {
- // can't read, continue
- continue
- }
- marshalledValue, err := toGoType((i+j)*32, input.Type, output)
- if err != nil {
- return err
- }
- if input.Type.T == ArrayTy {
- // combined index ('i' + 'j') need to be adjusted only by size of array, thus
- // we need to decrement 'j' because 'i' was incremented
- j += input.Type.Size - 1
- }
- reflectValue := reflect.ValueOf(marshalledValue)
-
- switch value.Kind() {
- case reflect.Struct:
- for j := 0; j < typ.NumField(); j++ {
- field := typ.Field(j)
- // TODO read tags: `abi:"fieldName"`
- if field.Name == strings.ToUpper(e.Inputs[i].Name[:1])+e.Inputs[i].Name[1:] {
- if err := set(value.Field(j), reflectValue, e.Inputs[i]); err != nil {
- return err
- }
- }
- }
- case reflect.Slice, reflect.Array:
- if value.Len() < i {
- return fmt.Errorf("abi: insufficient number of arguments for unpack, want %d, got %d", len(e.Inputs), value.Len())
- }
- v := value.Index(i)
- if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface {
- return fmt.Errorf("abi: cannot unmarshal %v in to %v", v.Type(), reflectValue.Type())
- }
- reflectValue := reflect.ValueOf(marshalledValue)
- if err := set(v.Elem(), reflectValue, e.Inputs[i]); err != nil {
- return err
- }
- default:
- return fmt.Errorf("abi: cannot unmarshal tuple in to %v", typ)
- }
- }
- return nil
-}
-
-func (e Event) isTupleReturn() bool { return len(e.Inputs) > 1 }
-
-func (e Event) singleUnpack(v interface{}, output []byte) error {
- // make sure the passed value is a pointer
- valueOf := reflect.ValueOf(v)
- if reflect.Ptr != valueOf.Kind() {
- return fmt.Errorf("abi: Unpack(non-pointer %T)", v)
- }
-
- if e.Inputs[0].Indexed {
- return fmt.Errorf("abi: attempting to unpack indexed variable into element.")
- }
-
- value := valueOf.Elem()
-
- marshalledValue, err := toGoType(0, e.Inputs[0].Type, output)
- if err != nil {
- return err
- }
- if err := set(value, reflect.ValueOf(marshalledValue), e.Inputs[0]); err != nil {
- return err
- }
- return nil
-}
diff --git a/accounts/abi/event_test.go b/accounts/abi/event_test.go
index a3899b4a6..cca61e433 100644
--- a/accounts/abi/event_test.go
+++ b/accounts/abi/event_test.go
@@ -18,15 +18,52 @@ package abi
import (
"bytes"
+ "encoding/hex"
+ "encoding/json"
+ "math/big"
"reflect"
"strings"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
+var jsonEventTransfer = []byte(`{
+ "anonymous": false,
+ "inputs": [
+ {
+ "indexed": true, "name": "from", "type": "address"
+ }, {
+ "indexed": true, "name": "to", "type": "address"
+ }, {
+ "indexed": false, "name": "value", "type": "uint256"
+ }],
+ "name": "Transfer",
+ "type": "event"
+}`)
+
+var jsonEventPledge = []byte(`{
+ "anonymous": false,
+ "inputs": [{
+ "indexed": false, "name": "who", "type": "address"
+ }, {
+ "indexed": false, "name": "wad", "type": "uint128"
+ }, {
+ "indexed": false, "name": "currency", "type": "bytes3"
+ }],
+ "name": "Pledge",
+ "type": "event"
+}`)
+
+// 1000000
+var transferData1 = "00000000000000000000000000000000000000000000000000000000000f4240"
+
+// "0x00Ce0d46d924CC8437c806721496599FC3FFA268", 2218516807680, "usd"
+var pledgeData1 = "00000000000000000000000000ce0d46d924cc8437c806721496599fc3ffa2680000000000000000000000000000000000000000000000000000020489e800007573640000000000000000000000000000000000000000000000000000000000"
+
func TestEventId(t *testing.T) {
var table = []struct {
definition string
@@ -77,3 +114,203 @@ func TestEventMultiValueWithArrayUnpack(t *testing.T) {
require.Equal(t, [2]uint8{1, 2}, rst.Value1)
require.Equal(t, uint8(3), rst.Value2)
}
+
+func TestEventTupleUnpack(t *testing.T) {
+
+ type EventTransfer struct {
+ Value *big.Int
+ }
+
+ type EventPledge struct {
+ Who common.Address
+ Wad *big.Int
+ Currency [3]byte
+ }
+
+ type BadEventPledge struct {
+ Who string
+ Wad int
+ Currency [3]byte
+ }
+
+ bigint := new(big.Int)
+ bigintExpected := big.NewInt(1000000)
+ bigintExpected2 := big.NewInt(2218516807680)
+ addr := common.HexToAddress("0x00Ce0d46d924CC8437c806721496599FC3FFA268")
+ var testCases = []struct {
+ data string
+ dest interface{}
+ expected interface{}
+ jsonLog []byte
+ error string
+ name string
+ }{{
+ transferData1,
+ &EventTransfer{},
+ &EventTransfer{Value: bigintExpected},
+ jsonEventTransfer,
+ "",
+ "Can unpack ERC20 Transfer event into structure",
+ }, {
+ transferData1,
+ &[]interface{}{&bigint},
+ &[]interface{}{&bigintExpected},
+ jsonEventTransfer,
+ "",
+ "Can unpack ERC20 Transfer event into slice",
+ }, {
+ pledgeData1,
+ &EventPledge{},
+ &EventPledge{
+ addr,
+ bigintExpected2,
+ [3]byte{'u', 's', 'd'}},
+ jsonEventPledge,
+ "",
+ "Can unpack Pledge event into structure",
+ }, {
+ pledgeData1,
+ &[]interface{}{&common.Address{}, &bigint, &[3]byte{}},
+ &[]interface{}{
+ &addr,
+ &bigintExpected2,
+ &[3]byte{'u', 's', 'd'}},
+ jsonEventPledge,
+ "",
+ "Can unpack Pledge event into slice",
+ }, {
+ pledgeData1,
+ &[3]interface{}{&common.Address{}, &bigint, &[3]byte{}},
+ &[3]interface{}{
+ &addr,
+ &bigintExpected2,
+ &[3]byte{'u', 's', 'd'}},
+ jsonEventPledge,
+ "",
+ "Can unpack Pledge event into an array",
+ }, {
+ pledgeData1,
+ &[]interface{}{new(int), 0, 0},
+ &[]interface{}{},
+ jsonEventPledge,
+ "abi: cannot unmarshal common.Address in to int",
+ "Can not unpack Pledge event into slice with wrong types",
+ }, {
+ pledgeData1,
+ &BadEventPledge{},
+ &BadEventPledge{},
+ jsonEventPledge,
+ "abi: cannot unmarshal common.Address in to string",
+ "Can not unpack Pledge event into struct with wrong filed types",
+ }, {
+ pledgeData1,
+ &[]interface{}{common.Address{}, new(big.Int)},
+ &[]interface{}{},
+ jsonEventPledge,
+ "abi: insufficient number of elements in the list/array for unpack, want 3, got 2",
+ "Can not unpack Pledge event into too short slice",
+ }, {
+ pledgeData1,
+ new(map[string]interface{}),
+ &[]interface{}{},
+ jsonEventPledge,
+ "abi: cannot unmarshal tuple into map[string]interface {}",
+ "Can not unpack Pledge event into map",
+ }}
+
+ for _, tc := range testCases {
+ assert := assert.New(t)
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ err := unpackTestEventData(tc.dest, tc.data, tc.jsonLog, assert)
+ if tc.error == "" {
+ assert.Nil(err, "Should be able to unpack event data.")
+ assert.Equal(tc.expected, tc.dest, tc.name)
+ } else {
+ assert.EqualError(err, tc.error)
+ }
+ })
+ }
+}
+
+func unpackTestEventData(dest interface{}, hexData string, jsonEvent []byte, assert *assert.Assertions) error {
+ data, err := hex.DecodeString(hexData)
+ assert.NoError(err, "Hex data should be a correct hex-string")
+ var e Event
+ assert.NoError(json.Unmarshal(jsonEvent, &e), "Should be able to unmarshal event ABI")
+ a := ABI{Events: map[string]Event{"e": e}}
+ return a.Unpack(dest, "e", data)
+}
+
+/*
+Taken from
+https://github.com/ethereum/go-ethereum/pull/15568
+*/
+
+type testResult struct {
+ Values [2]*big.Int
+ Value1 *big.Int
+ Value2 *big.Int
+}
+
+type testCase struct {
+ definition string
+ want testResult
+}
+
+func (tc testCase) encoded(intType, arrayType Type) []byte {
+ var b bytes.Buffer
+ if tc.want.Value1 != nil {
+ val, _ := intType.pack(reflect.ValueOf(tc.want.Value1))
+ b.Write(val)
+ }
+
+ if !reflect.DeepEqual(tc.want.Values, [2]*big.Int{nil, nil}) {
+ val, _ := arrayType.pack(reflect.ValueOf(tc.want.Values))
+ b.Write(val)
+ }
+ if tc.want.Value2 != nil {
+ val, _ := intType.pack(reflect.ValueOf(tc.want.Value2))
+ b.Write(val)
+ }
+ return b.Bytes()
+}
+
+// TestEventUnpackIndexed verifies that indexed field will be skipped by event decoder.
+func TestEventUnpackIndexed(t *testing.T) {
+ definition := `[{"name": "test", "type": "event", "inputs": [{"indexed": true, "name":"value1", "type":"uint8"},{"indexed": false, "name":"value2", "type":"uint8"}]}]`
+ type testStruct struct {
+ Value1 uint8
+ Value2 uint8
+ }
+ abi, err := JSON(strings.NewReader(definition))
+ require.NoError(t, err)
+ var b bytes.Buffer
+ b.Write(packNum(reflect.ValueOf(uint8(8))))
+ var rst testStruct
+ require.NoError(t, abi.Unpack(&rst, "test", b.Bytes()))
+ require.Equal(t, uint8(0), rst.Value1)
+ require.Equal(t, uint8(8), rst.Value2)
+}
+
+// TestEventIndexedWithArrayUnpack verifies that decoder will not overlow when static array is indexed input.
+func TestEventIndexedWithArrayUnpack(t *testing.T) {
+ definition := `[{"name": "test", "type": "event", "inputs": [{"indexed": true, "name":"value1", "type":"uint8[2]"},{"indexed": false, "name":"value2", "type":"string"}]}]`
+ type testStruct struct {
+ Value1 [2]uint8
+ Value2 string
+ }
+ abi, err := JSON(strings.NewReader(definition))
+ require.NoError(t, err)
+ var b bytes.Buffer
+ stringOut := "abc"
+ // number of fields that will be encoded * 32
+ b.Write(packNum(reflect.ValueOf(32)))
+ b.Write(packNum(reflect.ValueOf(len(stringOut))))
+ b.Write(common.RightPadBytes([]byte(stringOut), 32))
+
+ var rst testStruct
+ require.NoError(t, abi.Unpack(&rst, "test", b.Bytes()))
+ require.Equal(t, [2]uint8{0, 0}, rst.Value1)
+ require.Equal(t, stringOut, rst.Value2)
+}
diff --git a/accounts/abi/method.go b/accounts/abi/method.go
index 66e8751f3..f434ffdbe 100644
--- a/accounts/abi/method.go
+++ b/accounts/abi/method.go
@@ -18,13 +18,12 @@ package abi
import (
"fmt"
- "reflect"
"strings"
"github.com/ethereum/go-ethereum/crypto"
)
-// Callable method given a `Name` and whether the method is a constant.
+// Method represents a callable given a `Name` and whether the method is a constant.
// If the method is `Const` no transaction needs to be created for this
// particular Method call. It can easily be simulated using a local VM.
// For example a `Balance()` method only needs to retrieve something
@@ -35,137 +34,8 @@ import (
type Method struct {
Name string
Const bool
- Inputs []Argument
- Outputs []Argument
-}
-
-func (method Method) pack(args ...interface{}) ([]byte, error) {
- // Make sure arguments match up and pack them
- if len(args) != len(method.Inputs) {
- return nil, fmt.Errorf("argument count mismatch: %d for %d", len(args), len(method.Inputs))
- }
- // variable input is the output appended at the end of packed
- // output. This is used for strings and bytes types input.
- var variableInput []byte
-
- // input offset is the bytes offset for packed output
- inputOffset := 0
- for _, input := range method.Inputs {
- if input.Type.T == ArrayTy {
- inputOffset += (32 * input.Type.Size)
- } else {
- inputOffset += 32
- }
- }
-
- var ret []byte
- for i, a := range args {
- input := method.Inputs[i]
- // pack the input
- packed, err := input.Type.pack(reflect.ValueOf(a))
- if err != nil {
- return nil, fmt.Errorf("`%s` %v", method.Name, err)
- }
-
- // check for a slice type (string, bytes, slice)
- if input.Type.requiresLengthPrefix() {
- // calculate the offset
- offset := inputOffset + len(variableInput)
-
- // set the offset
- ret = append(ret, packNum(reflect.ValueOf(offset))...)
- // Append the packed output to the variable input. The variable input
- // will be appended at the end of the input.
- variableInput = append(variableInput, packed...)
- } else {
- // append the packed value to the input
- ret = append(ret, packed...)
- }
- }
- // append the variable input at the end of the packed input
- ret = append(ret, variableInput...)
-
- return ret, nil
-}
-
-// unpacks a method return tuple into a struct of corresponding go types
-//
-// Unpacking can be done into a struct or a slice/array.
-func (method Method) tupleUnpack(v interface{}, output []byte) error {
- // make sure the passed value is a pointer
- valueOf := reflect.ValueOf(v)
- if reflect.Ptr != valueOf.Kind() {
- return fmt.Errorf("abi: Unpack(non-pointer %T)", v)
- }
-
- var (
- value = valueOf.Elem()
- typ = value.Type()
- )
-
- j := 0
- for i := 0; i < len(method.Outputs); i++ {
- toUnpack := method.Outputs[i]
- marshalledValue, err := toGoType((i+j)*32, toUnpack.Type, output)
- if err != nil {
- return err
- }
- if toUnpack.Type.T == ArrayTy {
- // combined index ('i' + 'j') need to be adjusted only by size of array, thus
- // we need to decrement 'j' because 'i' was incremented
- j += toUnpack.Type.Size - 1
- }
- reflectValue := reflect.ValueOf(marshalledValue)
-
- switch value.Kind() {
- case reflect.Struct:
- for j := 0; j < typ.NumField(); j++ {
- field := typ.Field(j)
- // TODO read tags: `abi:"fieldName"`
- if field.Name == strings.ToUpper(method.Outputs[i].Name[:1])+method.Outputs[i].Name[1:] {
- if err := set(value.Field(j), reflectValue, method.Outputs[i]); err != nil {
- return err
- }
- }
- }
- case reflect.Slice, reflect.Array:
- if value.Len() < i {
- return fmt.Errorf("abi: insufficient number of arguments for unpack, want %d, got %d", len(method.Outputs), value.Len())
- }
- v := value.Index(i)
- if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface {
- return fmt.Errorf("abi: cannot unmarshal %v in to %v", v.Type(), reflectValue.Type())
- }
- reflectValue := reflect.ValueOf(marshalledValue)
- if err := set(v.Elem(), reflectValue, method.Outputs[i]); err != nil {
- return err
- }
- default:
- return fmt.Errorf("abi: cannot unmarshal tuple in to %v", typ)
- }
- }
- return nil
-}
-
-func (method Method) isTupleReturn() bool { return len(method.Outputs) > 1 }
-
-func (method Method) singleUnpack(v interface{}, output []byte) error {
- // make sure the passed value is a pointer
- valueOf := reflect.ValueOf(v)
- if reflect.Ptr != valueOf.Kind() {
- return fmt.Errorf("abi: Unpack(non-pointer %T)", v)
- }
-
- value := valueOf.Elem()
-
- marshalledValue, err := toGoType(0, method.Outputs[0].Type, output)
- if err != nil {
- return err
- }
- if err := set(value, reflect.ValueOf(marshalledValue), method.Outputs[0]); err != nil {
- return err
- }
- return nil
+ Inputs Arguments
+ Outputs Arguments
}
// Sig returns the methods string signature according to the ABI spec.
@@ -175,35 +45,35 @@ func (method Method) singleUnpack(v interface{}, output []byte) error {
// function foo(uint32 a, int b) = "foo(uint32,int256)"
//
// Please note that "int" is substitute for its canonical representation "int256"
-func (m Method) Sig() string {
- types := make([]string, len(m.Inputs))
+func (method Method) Sig() string {
+ types := make([]string, len(method.Inputs))
i := 0
- for _, input := range m.Inputs {
+ for _, input := range method.Inputs {
types[i] = input.Type.String()
i++
}
- return fmt.Sprintf("%v(%v)", m.Name, strings.Join(types, ","))
+ return fmt.Sprintf("%v(%v)", method.Name, strings.Join(types, ","))
}
-func (m Method) String() string {
- inputs := make([]string, len(m.Inputs))
- for i, input := range m.Inputs {
+func (method Method) String() string {
+ inputs := make([]string, len(method.Inputs))
+ for i, input := range method.Inputs {
inputs[i] = fmt.Sprintf("%v %v", input.Name, input.Type)
}
- outputs := make([]string, len(m.Outputs))
- for i, output := range m.Outputs {
+ outputs := make([]string, len(method.Outputs))
+ for i, output := range method.Outputs {
if len(output.Name) > 0 {
outputs[i] = fmt.Sprintf("%v ", output.Name)
}
outputs[i] += output.Type.String()
}
constant := ""
- if m.Const {
+ if method.Const {
constant = "constant "
}
- return fmt.Sprintf("function %v(%v) %sreturns(%v)", m.Name, strings.Join(inputs, ", "), constant, strings.Join(outputs, ", "))
+ return fmt.Sprintf("function %v(%v) %sreturns(%v)", method.Name, strings.Join(inputs, ", "), constant, strings.Join(outputs, ", "))
}
-func (m Method) Id() []byte {
- return crypto.Keccak256([]byte(m.Sig()))[:4]
+func (method Method) Id() []byte {
+ return crypto.Keccak256([]byte(method.Sig()))[:4]
}
diff --git a/accounts/abi/pack.go b/accounts/abi/pack.go
index 072e80536..36c58265b 100644
--- a/accounts/abi/pack.go
+++ b/accounts/abi/pack.go
@@ -48,9 +48,8 @@ func packElement(t Type, reflectValue reflect.Value) []byte {
case BoolTy:
if reflectValue.Bool() {
return math.PaddedBigBytes(common.Big1, 32)
- } else {
- return math.PaddedBigBytes(common.Big0, 32)
}
+ return math.PaddedBigBytes(common.Big0, 32)
case BytesTy:
if reflectValue.Kind() == reflect.Array {
reflectValue = mustArrayToByteSlice(reflectValue)
diff --git a/accounts/abi/reflect.go b/accounts/abi/reflect.go
index e953b77c1..7a9cdacd5 100644
--- a/accounts/abi/reflect.go
+++ b/accounts/abi/reflect.go
@@ -85,3 +85,28 @@ func set(dst, src reflect.Value, output Argument) error {
}
return nil
}
+
+// requireAssignable assures that `dest` is a pointer and it's not an interface.
+func requireAssignable(dst, src reflect.Value) error {
+ if dst.Kind() != reflect.Ptr && dst.Kind() != reflect.Interface {
+ return fmt.Errorf("abi: cannot unmarshal %v into %v", src.Type(), dst.Type())
+ }
+ return nil
+}
+
+// requireUnpackKind verifies preconditions for unpacking `args` into `kind`
+func requireUnpackKind(v reflect.Value, t reflect.Type, k reflect.Kind,
+ args Arguments) error {
+
+ switch k {
+ case reflect.Struct:
+ case reflect.Slice, reflect.Array:
+ if minLen := args.LengthNonIndexed(); v.Len() < minLen {
+ return fmt.Errorf("abi: insufficient number of elements in the list/array for unpack, want %d, got %d",
+ minLen, v.Len())
+ }
+ default:
+ return fmt.Errorf("abi: cannot unmarshal tuple into %v", t)
+ }
+ return nil
+}
diff --git a/accounts/abi/type.go b/accounts/abi/type.go
index fba10b96d..a1f13ffa2 100644
--- a/accounts/abi/type.go
+++ b/accounts/abi/type.go
@@ -24,6 +24,7 @@ import (
"strings"
)
+// Type enumerator
const (
IntTy byte = iota
UintTy
@@ -100,68 +101,65 @@ func NewType(t string) (typ Type, err error) {
return Type{}, fmt.Errorf("invalid formatting of array type")
}
return typ, err
+ }
+ // parse the type and size of the abi-type.
+ parsedType := typeRegex.FindAllStringSubmatch(t, -1)[0]
+ // varSize is the size of the variable
+ var varSize int
+ if len(parsedType[3]) > 0 {
+ var err error
+ varSize, err = strconv.Atoi(parsedType[2])
+ if err != nil {
+ return Type{}, fmt.Errorf("abi: error parsing variable size: %v", err)
+ }
} else {
- // parse the type and size of the abi-type.
- parsedType := typeRegex.FindAllStringSubmatch(t, -1)[0]
- // varSize is the size of the variable
- var varSize int
- if len(parsedType[3]) > 0 {
- var err error
- varSize, err = strconv.Atoi(parsedType[2])
- if err != nil {
- return Type{}, fmt.Errorf("abi: error parsing variable size: %v", err)
- }
- } else {
- if parsedType[0] == "uint" || parsedType[0] == "int" {
- // this should fail because it means that there's something wrong with
- // the abi type (the compiler should always format it to the size...always)
- return Type{}, fmt.Errorf("unsupported arg type: %s", t)
- }
+ if parsedType[0] == "uint" || parsedType[0] == "int" {
+ // this should fail because it means that there's something wrong with
+ // the abi type (the compiler should always format it to the size...always)
+ return Type{}, fmt.Errorf("unsupported arg type: %s", t)
}
- // varType is the parsed abi type
- varType := parsedType[1]
-
- switch varType {
- case "int":
- typ.Kind, typ.Type = reflectIntKindAndType(false, varSize)
- typ.Size = varSize
- typ.T = IntTy
- case "uint":
- typ.Kind, typ.Type = reflectIntKindAndType(true, varSize)
- typ.Size = varSize
- typ.T = UintTy
- case "bool":
- typ.Kind = reflect.Bool
- typ.T = BoolTy
- typ.Type = reflect.TypeOf(bool(false))
- case "address":
- typ.Kind = reflect.Array
- typ.Type = address_t
- typ.Size = 20
- typ.T = AddressTy
- case "string":
- typ.Kind = reflect.String
- typ.Type = reflect.TypeOf("")
- typ.T = StringTy
- case "bytes":
- if varSize == 0 {
- typ.T = BytesTy
- typ.Kind = reflect.Slice
- typ.Type = reflect.SliceOf(reflect.TypeOf(byte(0)))
- } else {
- typ.T = FixedBytesTy
- typ.Kind = reflect.Array
- typ.Size = varSize
- typ.Type = reflect.ArrayOf(varSize, reflect.TypeOf(byte(0)))
- }
- case "function":
+ }
+ // varType is the parsed abi type
+ switch varType := parsedType[1]; varType {
+ case "int":
+ typ.Kind, typ.Type = reflectIntKindAndType(false, varSize)
+ typ.Size = varSize
+ typ.T = IntTy
+ case "uint":
+ typ.Kind, typ.Type = reflectIntKindAndType(true, varSize)
+ typ.Size = varSize
+ typ.T = UintTy
+ case "bool":
+ typ.Kind = reflect.Bool
+ typ.T = BoolTy
+ typ.Type = reflect.TypeOf(bool(false))
+ case "address":
+ typ.Kind = reflect.Array
+ typ.Type = address_t
+ typ.Size = 20
+ typ.T = AddressTy
+ case "string":
+ typ.Kind = reflect.String
+ typ.Type = reflect.TypeOf("")
+ typ.T = StringTy
+ case "bytes":
+ if varSize == 0 {
+ typ.T = BytesTy
+ typ.Kind = reflect.Slice
+ typ.Type = reflect.SliceOf(reflect.TypeOf(byte(0)))
+ } else {
+ typ.T = FixedBytesTy
typ.Kind = reflect.Array
- typ.T = FunctionTy
- typ.Size = 24
- typ.Type = reflect.ArrayOf(24, reflect.TypeOf(byte(0)))
- default:
- return Type{}, fmt.Errorf("unsupported arg type: %s", t)
+ typ.Size = varSize
+ typ.Type = reflect.ArrayOf(varSize, reflect.TypeOf(byte(0)))
}
+ case "function":
+ typ.Kind = reflect.Array
+ typ.T = FunctionTy
+ typ.Size = 24
+ typ.Type = reflect.ArrayOf(24, reflect.TypeOf(byte(0)))
+ default:
+ return Type{}, fmt.Errorf("unsupported arg type: %s", t)
}
return
diff --git a/accounts/abi/unpack.go b/accounts/abi/unpack.go
index 051fc1916..80efb3f7e 100644
--- a/accounts/abi/unpack.go
+++ b/accounts/abi/unpack.go
@@ -25,15 +25,6 @@ import (
"github.com/ethereum/go-ethereum/common"
)
-// unpacker is a utility interface that enables us to have
-// abstraction between events and methods and also to properly
-// "unpack" them; e.g. events use Inputs, methods use Outputs.
-type unpacker interface {
- tupleUnpack(v interface{}, output []byte) error
- singleUnpack(v interface{}, output []byte) error
- isTupleReturn() bool
-}
-
// reads the integer based on its kind
func readInteger(kind reflect.Kind, b []byte) interface{} {
switch kind {
@@ -79,7 +70,7 @@ func readBool(word []byte) (bool, error) {
// This enforces that standard by always presenting it as a 24-array (address + sig = 24 bytes)
func readFunctionType(t Type, word []byte) (funcTy [24]byte, err error) {
if t.T != FunctionTy {
- return [24]byte{}, fmt.Errorf("abi: invalid type in call to make function type byte array.")
+ return [24]byte{}, fmt.Errorf("abi: invalid type in call to make function type byte array")
}
if garbage := binary.BigEndian.Uint64(word[24:32]); garbage != 0 {
err = fmt.Errorf("abi: got improperly encoded function type, got %v", word)
@@ -92,7 +83,7 @@ func readFunctionType(t Type, word []byte) (funcTy [24]byte, err error) {
// through reflection, creates a fixed array to be read from
func readFixedBytes(t Type, word []byte) (interface{}, error) {
if t.T != FixedBytesTy {
- return nil, fmt.Errorf("abi: invalid type in call to make fixed byte array.")
+ return nil, fmt.Errorf("abi: invalid type in call to make fixed byte array")
}
// convert
array := reflect.New(t.Type).Elem()
diff --git a/accounts/abi/unpack_test.go b/accounts/abi/unpack_test.go
index 14393d230..391095073 100644
--- a/accounts/abi/unpack_test.go
+++ b/accounts/abi/unpack_test.go
@@ -27,6 +27,7 @@ import (
"testing"
"github.com/ethereum/go-ethereum/common"
+ "github.com/stretchr/testify/require"
)
type unpackTest struct {
@@ -286,56 +287,83 @@ func TestUnpack(t *testing.T) {
}
}
-func TestMultiReturnWithStruct(t *testing.T) {
+type methodMultiOutput struct {
+ Int *big.Int
+ String string
+}
+
+func methodMultiReturn(require *require.Assertions) (ABI, []byte, methodMultiOutput) {
const definition = `[
{ "name" : "multi", "constant" : false, "outputs": [ { "name": "Int", "type": "uint256" }, { "name": "String", "type": "string" } ] }]`
+ var expected = methodMultiOutput{big.NewInt(1), "hello"}
abi, err := JSON(strings.NewReader(definition))
- if err != nil {
- t.Fatal(err)
- }
-
+ require.NoError(err)
// using buff to make the code readable
buff := new(bytes.Buffer)
buff.Write(common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000001"))
buff.Write(common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000040"))
buff.Write(common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000005"))
- stringOut := "hello"
- buff.Write(common.RightPadBytes([]byte(stringOut), 32))
-
- var inter struct {
- Int *big.Int
- String string
- }
- err = abi.Unpack(&inter, "multi", buff.Bytes())
- if err != nil {
- t.Error(err)
- }
-
- if inter.Int == nil || inter.Int.Cmp(big.NewInt(1)) != 0 {
- t.Error("expected Int to be 1 got", inter.Int)
- }
-
- if inter.String != stringOut {
- t.Error("expected String to be", stringOut, "got", inter.String)
- }
+ buff.Write(common.RightPadBytes([]byte(expected.String), 32))
+ return abi, buff.Bytes(), expected
+}
- var reversed struct {
+func TestMethodMultiReturn(t *testing.T) {
+ type reversed struct {
String string
Int *big.Int
}
- err = abi.Unpack(&reversed, "multi", buff.Bytes())
- if err != nil {
- t.Error(err)
- }
-
- if reversed.Int == nil || reversed.Int.Cmp(big.NewInt(1)) != 0 {
- t.Error("expected Int to be 1 got", reversed.Int)
- }
-
- if reversed.String != stringOut {
- t.Error("expected String to be", stringOut, "got", reversed.String)
+ abi, data, expected := methodMultiReturn(require.New(t))
+ bigint := new(big.Int)
+ var testCases = []struct {
+ dest interface{}
+ expected interface{}
+ error string
+ name string
+ }{{
+ &methodMultiOutput{},
+ &expected,
+ "",
+ "Can unpack into structure",
+ }, {
+ &reversed{},
+ &reversed{expected.String, expected.Int},
+ "",
+ "Can unpack into reversed structure",
+ }, {
+ &[]interface{}{&bigint, new(string)},
+ &[]interface{}{&expected.Int, &expected.String},
+ "",
+ "Can unpack into a slice",
+ }, {
+ &[2]interface{}{&bigint, new(string)},
+ &[2]interface{}{&expected.Int, &expected.String},
+ "",
+ "Can unpack into an array",
+ }, {
+ &[]interface{}{new(int), new(int)},
+ &[]interface{}{&expected.Int, &expected.String},
+ "abi: cannot unmarshal *big.Int in to int",
+ "Can not unpack into a slice with wrong types",
+ }, {
+ &[]interface{}{new(int)},
+ &[]interface{}{},
+ "abi: insufficient number of elements in the list/array for unpack, want 2, got 1",
+ "Can not unpack into a slice with wrong types",
+ }}
+ for _, tc := range testCases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ require := require.New(t)
+ err := abi.Unpack(tc.dest, "multi", data)
+ if tc.error == "" {
+ require.Nil(err, "Should be able to unpack method outputs.")
+ require.Equal(tc.expected, tc.dest)
+ } else {
+ require.EqualError(err, tc.error)
+ }
+ })
}
}