-
Notifications
You must be signed in to change notification settings - Fork 21.9k
accounts/abi refactor #15731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
accounts/abi refactor #15731
Changes from all commits
3511904
9becba5
0ed8b83
95461e8
1afca33
81d4caf
73d4a57
c095c87
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to return a pointer? It's a small structure. How about returning
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, that would work too... Don't know why that would be better really... |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about using testify? 6 lines above could be substituted with: Also this solves another issue: if the first
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I haven't gotten used to testify yet, that's the reason :) |
||
| } | ||
|
|
||
| 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) | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above. |
||
| } | ||
| } | ||
|
|
||
|
|
@@ -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) | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can use testify: |
||
| 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())) | ||
| } | ||
| } | ||
|
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some linters will complain if there is no consistency when implementing methods for a type. I advise that you call all methods |
||
| // 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 | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't there be a lookup table for method.Id() <=> method ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There could be, but I don't think this will be extensively used; I'll use it from the signer, but it's a one-off operation. So might as well do it lazily.