From 0050f84142e54886076c01fa074bbb8baffe76b3 Mon Sep 17 00:00:00 2001 From: Daniel Liu Date: Tue, 14 Jan 2025 10:56:09 +0800 Subject: [PATCH] accounts/abi: enable struct golang binding generation (#18491) * accounts/abi, cmd/abigen: support tuple accounts/abi/bind, cmd/abigen: add objc back accounts/abi/bind: use byte[24] as function indicator accounts/abi/bind: resolve struct slice or array accounts/abi/bind: remove sort logic accounts: fix issues in abi * accounts/abi: address comment --- accounts/abi/argument.go | 17 ++++- accounts/abi/bind/bind.go | 127 +++++++++++++++++++++++++++++++--- accounts/abi/bind/template.go | 67 ++++++++++++------ accounts/abi/method_test.go | 73 ++++++++++++++++++- accounts/abi/reflect.go | 8 +++ accounts/abi/type.go | 9 +-- accounts/abi/unpack_test.go | 16 ++--- 7 files changed, 264 insertions(+), 53 deletions(-) diff --git a/accounts/abi/argument.go b/accounts/abi/argument.go index ff1265a8cb..8cfa8db1a9 100644 --- a/accounts/abi/argument.go +++ b/accounts/abi/argument.go @@ -131,11 +131,22 @@ func unpack(t *Type, dst interface{}, src interface{}) error { dstVal = reflect.ValueOf(dst).Elem() srcVal = reflect.ValueOf(src) ) - - if t.T != TupleTy && !((t.T == SliceTy || t.T == ArrayTy) && t.Elem.T == TupleTy) { + tuple, typ := false, t + for { + if typ.T == SliceTy || typ.T == ArrayTy { + typ = typ.Elem + continue + } + tuple = typ.T == TupleTy + break + } + if !tuple { return set(dstVal, srcVal) } + // Dereferences interface or pointer wrapper + dstVal = indirectInterfaceOrPtr(dstVal) + switch t.T { case TupleTy: if dstVal.Kind() != reflect.Struct { @@ -203,7 +214,7 @@ func (arguments Arguments) unpackAtomic(v interface{}, marshalledValues interfac argument := arguments.NonIndexed()[0] elem := reflect.ValueOf(v).Elem() - if elem.Kind() == reflect.Struct { + if elem.Kind() == reflect.Struct && argument.Type.T != TupleTy { fieldmap, err := mapArgNamesToStructFields([]string{argument.Name}, elem) if err != nil { return err diff --git a/accounts/abi/bind/bind.go b/accounts/abi/bind/bind.go index 92dd5ba374..d2c20489d3 100644 --- a/accounts/abi/bind/bind.go +++ b/accounts/abi/bind/bind.go @@ -65,11 +65,12 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] return r }, abis[i]) - // Extract the call and transact methods; events; and sort them alphabetically + // Extract the call and transact methods; events, struct definitions; and sort them alphabetically var ( calls = make(map[string]*tmplMethod) transacts = make(map[string]*tmplMethod) events = make(map[string]*tmplEvent) + structs = make(map[string]*tmplStruct) ) for _, original := range evmABI.Methods { // Normalize the method for capital cases and non-anonymous inputs/outputs @@ -82,6 +83,9 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] if input.Name == "" { normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j) } + if _, exist := structs[input.Type.String()]; input.Type.T == abi.TupleTy && !exist { + bindStructType[lang](input.Type, structs) + } } normalized.Outputs = make([]abi.Argument, len(original.Outputs)) copy(normalized.Outputs, original.Outputs) @@ -89,6 +93,9 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] if output.Name != "" { normalized.Outputs[j].Name = capitalise(output.Name) } + if _, exist := structs[output.Type.String()]; output.Type.T == abi.TupleTy && !exist { + bindStructType[lang](output.Type, structs) + } } // Append the methods to the call or transact lists if original.Const { @@ -114,6 +121,9 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] if input.Name == "" { normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j) } + if _, exist := structs[input.Type.String()]; input.Type.T == abi.TupleTy && !exist { + bindStructType[lang](input.Type, structs) + } } } // Append the event to the accumulator list @@ -129,6 +139,7 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] Transacts: transacts, Events: events, Libraries: make(map[string]string), + Structs: structs, } // Function 4-byte signatures are stored in the same sequence // as types, if available. @@ -167,6 +178,8 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] "bindtype": bindType[lang], "bindtopictype": bindTopicType[lang], "namedtype": namedType[lang], + "formatmethod": formatMethod, + "formatevent": formatEvent, "capitalise": capitalise, "decapitalise": decapitalise, } @@ -188,7 +201,7 @@ func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string] // bindType is a set of type binders that convert Solidity types to some supported // programming language types. -var bindType = map[Lang]func(kind abi.Type) string{ +var bindType = map[Lang]func(kind abi.Type, structs map[string]*tmplStruct) string{ LangGo: bindTypeGo, } @@ -219,13 +232,14 @@ func bindBasicTypeGo(kind abi.Type) string { // bindTypeGo converts solidity types to Go ones. Since there is no clear mapping // from all Solidity types to Go ones (e.g. uint17), those that cannot be exactly // mapped will use an upscaled type (e.g. BigDecimal). -func bindTypeGo(kind abi.Type) string { - // todo(rjl493456442) tuple +func bindTypeGo(kind abi.Type, structs map[string]*tmplStruct) string { switch kind.T { + case abi.TupleTy: + return structs[kind.String()].Name case abi.ArrayTy: - return fmt.Sprintf("[%d]", kind.Size) + bindTypeGo(*kind.Elem) + return fmt.Sprintf("[%d]", kind.Size) + bindTypeGo(*kind.Elem, structs) case abi.SliceTy: - return "[]" + bindTypeGo(*kind.Elem) + return "[]" + bindTypeGo(*kind.Elem, structs) default: return bindBasicTypeGo(kind) } @@ -233,20 +247,55 @@ func bindTypeGo(kind abi.Type) string { // bindTopicType is a set of type binders that convert Solidity types to some // supported programming language topic types. -var bindTopicType = map[Lang]func(kind abi.Type) string{ +var bindTopicType = map[Lang]func(kind abi.Type, structs map[string]*tmplStruct) string{ LangGo: bindTopicTypeGo, } // bindTypeGo converts a Solidity topic type to a Go one. It is almost the same // funcionality as for simple types, but dynamic types get converted to hashes. -func bindTopicTypeGo(kind abi.Type) string { - bound := bindTypeGo(kind) +func bindTopicTypeGo(kind abi.Type, structs map[string]*tmplStruct) string { + bound := bindTypeGo(kind, structs) if bound == "string" || bound == "[]byte" { bound = "common.Hash" } return bound } +// bindStructType is a set of type binders that convert Solidity tuple types to some supported +// programming language struct definition. +var bindStructType = map[Lang]func(kind abi.Type, structs map[string]*tmplStruct) string{ + LangGo: bindStructTypeGo, +} + +// bindStructTypeGo converts a Solidity tuple type to a Go one and records the mapping +// in the given map. +// Notably, this function will resolve and record nested struct recursively. +func bindStructTypeGo(kind abi.Type, structs map[string]*tmplStruct) string { + switch kind.T { + case abi.TupleTy: + if s, exist := structs[kind.String()]; exist { + return s.Name + } + var fields []*tmplField + for i, elem := range kind.TupleElems { + field := bindStructTypeGo(*elem, structs) + fields = append(fields, &tmplField{Type: field, Name: capitalise(kind.TupleRawNames[i]), SolKind: *elem}) + } + name := fmt.Sprintf("Struct%d", len(structs)) + structs[kind.String()] = &tmplStruct{ + Name: name, + Fields: fields, + } + return name + case abi.ArrayTy: + return fmt.Sprintf("[%d]", kind.Size) + bindStructTypeGo(*kind.Elem, structs) + case abi.SliceTy: + return "[]" + bindStructTypeGo(*kind.Elem, structs) + default: + return bindBasicTypeGo(kind) + } +} + // namedType is a set of functions that transform language specific types to // named versions that my be used inside method names. var namedType = map[Lang]func(string, abi.Type) string{ @@ -296,3 +345,63 @@ func structured(args abi.Arguments) bool { } return true } + +// resolveArgName converts a raw argument representation into a user friendly format. +func resolveArgName(arg abi.Argument, structs map[string]*tmplStruct) string { + var ( + prefix string + embedded string + typ = &arg.Type + ) +loop: + for { + switch typ.T { + case abi.SliceTy: + prefix += "[]" + case abi.ArrayTy: + prefix += fmt.Sprintf("[%d]", typ.Size) + default: + embedded = typ.String() + break loop + } + typ = typ.Elem + } + if s, exist := structs[embedded]; exist { + return prefix + s.Name + } else { + return arg.Type.String() + } +} + +// formatMethod transforms raw method representation into a user friendly one. +func formatMethod(method abi.Method, structs map[string]*tmplStruct) string { + inputs := make([]string, len(method.Inputs)) + for i, input := range method.Inputs { + inputs[i] = fmt.Sprintf("%v %v", resolveArgName(input, structs), input.Name) + } + outputs := make([]string, len(method.Outputs)) + for i, output := range method.Outputs { + outputs[i] = resolveArgName(output, structs) + if len(output.Name) > 0 { + outputs[i] += fmt.Sprintf(" %v", output.Name) + } + } + constant := "" + if method.Const { + constant = "constant " + } + return fmt.Sprintf("function %v(%v) %sreturns(%v)", method.Name, strings.Join(inputs, ", "), constant, strings.Join(outputs, ", ")) +} + +// formatEvent transforms raw event representation into a user friendly one. +func formatEvent(event abi.Event, structs map[string]*tmplStruct) string { + inputs := make([]string, len(event.Inputs)) + for i, input := range event.Inputs { + if input.Indexed { + inputs[i] = fmt.Sprintf("%v indexed %v", resolveArgName(input, structs), input.Name) + } else { + inputs[i] = fmt.Sprintf("%v %v", resolveArgName(input, structs), input.Name) + } + } + return fmt.Sprintf("event %v(%v)", event.Name, strings.Join(inputs, ", ")) +} diff --git a/accounts/abi/bind/template.go b/accounts/abi/bind/template.go index e743d4064b..f941d595ad 100644 --- a/accounts/abi/bind/template.go +++ b/accounts/abi/bind/template.go @@ -36,6 +36,7 @@ type tmplContract struct { Transacts map[string]*tmplMethod // Contract calls that write state data Events map[string]*tmplEvent // Contract events accessors Libraries map[string]string // Same as tmplData, but filtered to only keep what the contract needs + Structs map[string]*tmplStruct // Contract struct type definitions Library bool } @@ -53,6 +54,21 @@ type tmplEvent struct { Normalized abi.Event // Normalized version of the parsed fields } +// tmplField is a wrapper around a struct field with binding language +// struct type definition and relative filed name. +type tmplField struct { + Type string // Field type representation depends on target binding language + Name string // Field name converted from the raw user-defined field name + SolKind abi.Type // Raw abi type information +} + +// tmplStruct is a wrapper around an abi.tuple contains a auto-generated +// struct name. +type tmplStruct struct { + Name string // Auto-generated struct name(We can't obtain the raw struct name through abi) + Fields []*tmplField // Struct fields definition depends on the binding language. +} + // tmplSource is language to template mapping containing all the supported // programming languages the package can generate to. var tmplSource = map[Lang]string{ @@ -91,6 +107,7 @@ var ( ) {{range $contract := .Contracts}} + {{$structs := $contract.Structs}} // {{.Type}}ABI is the input ABI used to generate the binding from. const {{.Type}}ABI = "{{.InputABI}}" @@ -107,7 +124,7 @@ var ( var {{.Type}}Bin = "0x{{.InputBin}}" // Deploy{{.Type}} deploys a new Ethereum contract, binding an instance of {{.Type}} to it. - func Deploy{{.Type}}(auth *bind.TransactOpts, backend bind.ContractBackend {{range .Constructor.Inputs}}, {{.Name}} {{bindtype .Type}}{{end}}) (common.Address, *types.Transaction, *{{.Type}}, error) { + func Deploy{{.Type}}(auth *bind.TransactOpts, backend bind.ContractBackend {{range .Constructor.Inputs}}, {{.Name}} {{bindtype .Type $structs}}{{end}}) (common.Address, *types.Transaction, *{{.Type}}, error) { parsed, err := abi.JSON(strings.NewReader({{.Type}}ABI)) if err != nil { return common.Address{}, nil, nil, err @@ -128,7 +145,7 @@ var ( type {{.Type}} struct { {{.Type}}Caller // Read-only binding to the contract {{.Type}}Transactor // Write-only binding to the contract - {{.Type}}Filterer // Log filterer for contract events + {{.Type}}Filterer // Log filterer for contract events } // {{.Type}}Caller is an auto generated read-only Go binding around an Ethereum contract. @@ -266,16 +283,24 @@ var ( return _{{$contract.Type}}.Contract.contract.Transact(opts, method, params...) } + {{range .Structs}} + // {{.Name}} is an auto generated low-level Go binding around an user-defined struct. + type {{.Name}} struct { + {{range $field := .Fields}} + {{$field.Name}} {{$field.Type}}{{end}} + } + {{end}} + {{range .Calls}} // {{.Normalized.Name}} is a free data retrieval call binding the contract method 0x{{printf "%x" .Original.Id}}. // - // Solidity: {{.Original.String}} - func (_{{$contract.Type}} *{{$contract.Type}}Caller) {{.Normalized.Name}}(opts *bind.CallOpts {{range .Normalized.Inputs}}, {{.Name}} {{bindtype .Type}} {{end}}) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type}};{{end}} },{{else}}{{range .Normalized.Outputs}}{{bindtype .Type}},{{end}}{{end}} error) { + // Solidity: {{formatmethod .Original $structs}} + func (_{{$contract.Type}} *{{$contract.Type}}Caller) {{.Normalized.Name}}(opts *bind.CallOpts {{range .Normalized.Inputs}}, {{.Name}} {{bindtype .Type $structs}} {{end}}) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type $structs}};{{end}} },{{else}}{{range .Normalized.Outputs}}{{bindtype .Type $structs}},{{end}}{{end}} error) { {{if .Structured}}ret := new(struct{ - {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type}} + {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type $structs}} {{end}} }){{else}}var ( - {{range $i, $_ := .Normalized.Outputs}}ret{{$i}} = new({{bindtype .Type}}) + {{range $i, $_ := .Normalized.Outputs}}ret{{$i}} = new({{bindtype .Type $structs}}) {{end}} ){{end}} out := {{if .Structured}}ret{{else}}{{if eq (len .Normalized.Outputs) 1}}ret0{{else}}&[]interface{}{ @@ -288,15 +313,15 @@ var ( // {{.Normalized.Name}} is a free data retrieval call binding the contract method 0x{{printf "%x" .Original.Id}}. // - // Solidity: {{.Original.String}} - func (_{{$contract.Type}} *{{$contract.Type}}Session) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type}} {{end}}) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type}};{{end}} }, {{else}} {{range .Normalized.Outputs}}{{bindtype .Type}},{{end}} {{end}} error) { + // Solidity: {{formatmethod .Original $structs}} + func (_{{$contract.Type}} *{{$contract.Type}}Session) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type $structs}} {{end}}) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type $structs}};{{end}} }, {{else}} {{range .Normalized.Outputs}}{{bindtype .Type $structs}},{{end}} {{end}} error) { return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.CallOpts {{range .Normalized.Inputs}}, {{.Name}}{{end}}) } // {{.Normalized.Name}} is a free data retrieval call binding the contract method 0x{{printf "%x" .Original.Id}}. // - // Solidity: {{.Original.String}} - func (_{{$contract.Type}} *{{$contract.Type}}CallerSession) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type}} {{end}}) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type}};{{end}} }, {{else}} {{range .Normalized.Outputs}}{{bindtype .Type}},{{end}} {{end}} error) { + // Solidity: {{formatmethod .Original $structs}} + func (_{{$contract.Type}} *{{$contract.Type}}CallerSession) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type $structs}} {{end}}) ({{if .Structured}}struct{ {{range .Normalized.Outputs}}{{.Name}} {{bindtype .Type $structs}};{{end}} }, {{else}} {{range .Normalized.Outputs}}{{bindtype .Type $structs}},{{end}} {{end}} error) { return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.CallOpts {{range .Normalized.Inputs}}, {{.Name}}{{end}}) } {{end}} @@ -304,22 +329,22 @@ var ( {{range .Transacts}} // {{.Normalized.Name}} is a paid mutator transaction binding the contract method 0x{{printf "%x" .Original.Id}}. // - // Solidity: {{.Original.String}} - func (_{{$contract.Type}} *{{$contract.Type}}Transactor) {{.Normalized.Name}}(opts *bind.TransactOpts {{range .Normalized.Inputs}}, {{.Name}} {{bindtype .Type}} {{end}}) (*types.Transaction, error) { + // Solidity: {{formatmethod .Original $structs}} + func (_{{$contract.Type}} *{{$contract.Type}}Transactor) {{.Normalized.Name}}(opts *bind.TransactOpts {{range .Normalized.Inputs}}, {{.Name}} {{bindtype .Type $structs}} {{end}}) (*types.Transaction, error) { return _{{$contract.Type}}.contract.Transact(opts, "{{.Original.Name}}" {{range .Normalized.Inputs}}, {{.Name}}{{end}}) } // {{.Normalized.Name}} is a paid mutator transaction binding the contract method 0x{{printf "%x" .Original.Id}}. // - // Solidity: {{.Original.String}} - func (_{{$contract.Type}} *{{$contract.Type}}Session) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type}} {{end}}) (*types.Transaction, error) { + // Solidity: {{formatmethod .Original $structs}} + func (_{{$contract.Type}} *{{$contract.Type}}Session) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type $structs}} {{end}}) (*types.Transaction, error) { return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.TransactOpts {{range $i, $_ := .Normalized.Inputs}}, {{.Name}}{{end}}) } // {{.Normalized.Name}} is a paid mutator transaction binding the contract method 0x{{printf "%x" .Original.Id}}. // - // Solidity: {{.Original.String}} - func (_{{$contract.Type}} *{{$contract.Type}}TransactorSession) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type}} {{end}}) (*types.Transaction, error) { + // Solidity: {{formatmethod .Original $structs}} + func (_{{$contract.Type}} *{{$contract.Type}}TransactorSession) {{.Normalized.Name}}({{range $i, $_ := .Normalized.Inputs}}{{if ne $i 0}},{{end}} {{.Name}} {{bindtype .Type $structs}} {{end}}) (*types.Transaction, error) { return _{{$contract.Type}}.Contract.{{.Normalized.Name}}(&_{{$contract.Type}}.TransactOpts {{range $i, $_ := .Normalized.Inputs}}, {{.Name}}{{end}}) } {{end}} @@ -391,14 +416,14 @@ var ( // {{$contract.Type}}{{.Normalized.Name}} represents a {{.Normalized.Name}} event raised by the {{$contract.Type}} contract. type {{$contract.Type}}{{.Normalized.Name}} struct { {{range .Normalized.Inputs}} - {{capitalise .Name}} {{if .Indexed}}{{bindtopictype .Type}}{{else}}{{bindtype .Type}}{{end}}; {{end}} + {{capitalise .Name}} {{if .Indexed}}{{bindtopictype .Type $structs}}{{else}}{{bindtype .Type $structs}}{{end}}; {{end}} Raw types.Log // Blockchain specific contextual infos } // Filter{{.Normalized.Name}} is a free log retrieval operation binding the contract event 0x{{printf "%x" .Original.Id}}. // - // Solidity: {{.Original.String}} - func (_{{$contract.Type}} *{{$contract.Type}}Filterer) Filter{{.Normalized.Name}}(opts *bind.FilterOpts{{range .Normalized.Inputs}}{{if .Indexed}}, {{.Name}} []{{bindtype .Type}}{{end}}{{end}}) (*{{$contract.Type}}{{.Normalized.Name}}Iterator, error) { + // Solidity: {{formatevent .Original $structs}} + func (_{{$contract.Type}} *{{$contract.Type}}Filterer) Filter{{.Normalized.Name}}(opts *bind.FilterOpts{{range .Normalized.Inputs}}{{if .Indexed}}, {{.Name}} []{{bindtype .Type $structs}}{{end}}{{end}}) (*{{$contract.Type}}{{.Normalized.Name}}Iterator, error) { {{range .Normalized.Inputs}} {{if .Indexed}}var {{.Name}}Rule []interface{} for _, {{.Name}}Item := range {{.Name}} { @@ -414,8 +439,8 @@ var ( // Watch{{.Normalized.Name}} is a free log subscription operation binding the contract event 0x{{printf "%x" .Original.Id}}. // - // Solidity: {{.Original.String}} - func (_{{$contract.Type}} *{{$contract.Type}}Filterer) Watch{{.Normalized.Name}}(opts *bind.WatchOpts, sink chan<- *{{$contract.Type}}{{.Normalized.Name}}{{range .Normalized.Inputs}}{{if .Indexed}}, {{.Name}} []{{bindtype .Type}}{{end}}{{end}}) (event.Subscription, error) { + // Solidity: {{formatevent .Original $structs}} + func (_{{$contract.Type}} *{{$contract.Type}}Filterer) Watch{{.Normalized.Name}}(opts *bind.WatchOpts, sink chan<- *{{$contract.Type}}{{.Normalized.Name}}{{range .Normalized.Inputs}}{{if .Indexed}}, {{.Name}} []{{bindtype .Type $structs}}{{end}}{{end}}) (event.Subscription, error) { {{range .Normalized.Inputs}} {{if .Indexed}}var {{.Name}}Rule []interface{} for _, {{.Name}}Item := range {{.Name}} { diff --git a/accounts/abi/method_test.go b/accounts/abi/method_test.go index a98f1cd31f..92c360f0d2 100644 --- a/accounts/abi/method_test.go +++ b/accounts/abi/method_test.go @@ -23,9 +23,13 @@ import ( const methoddata = ` [ - { "type" : "function", "name" : "balance", "constant" : true }, - { "type" : "function", "name" : "send", "constant" : false, "inputs" : [ { "name" : "amount", "type" : "uint256" } ] }, - { "type" : "function", "name" : "transfer", "constant" : false, "inputs" : [ { "name" : "from", "type" : "address" }, { "name" : "to", "type" : "address" }, { "name" : "value", "type" : "uint256" } ], "outputs" : [ { "name" : "success", "type" : "bool" } ] } + {"type": "function", "name": "balance", "constant": true }, + {"type": "function", "name": "send", "constant": false, "inputs": [{ "name": "amount", "type": "uint256" }]}, + {"type": "function", "name": "transfer", "constant": false, "inputs": [{"name": "from", "type": "address"}, {"name": "to", "type": "address"}, {"name": "value", "type": "uint256"}], "outputs": [{"name": "success", "type": "bool"}]}, + {"constant":false,"inputs":[{"components":[{"name":"x","type":"uint256"},{"name":"y","type":"uint256"}],"name":"a","type":"tuple"}],"name":"tuple","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}, + {"constant":false,"inputs":[{"components":[{"name":"x","type":"uint256"},{"name":"y","type":"uint256"}],"name":"a","type":"tuple[]"}],"name":"tupleSlice","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}, + {"constant":false,"inputs":[{"components":[{"name":"x","type":"uint256"},{"name":"y","type":"uint256"}],"name":"a","type":"tuple[5]"}],"name":"tupleArray","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"}, + {"constant":false,"inputs":[{"components":[{"name":"x","type":"uint256"},{"name":"y","type":"uint256"}],"name":"a","type":"tuple[5][]"}],"name":"complexTuple","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"} ]` func TestMethodString(t *testing.T) { @@ -45,6 +49,22 @@ func TestMethodString(t *testing.T) { method: "transfer", expectation: "function transfer(address from, address to, uint256 value) returns(bool success)", }, + { + method: "tuple", + expectation: "function tuple((uint256,uint256) a) returns()", + }, + { + method: "tupleArray", + expectation: "function tupleArray((uint256,uint256)[5] a) returns()", + }, + { + method: "tupleSlice", + expectation: "function tupleSlice((uint256,uint256)[] a) returns()", + }, + { + method: "complexTuple", + expectation: "function complexTuple((uint256,uint256)[5][] a) returns()", + }, } abi, err := JSON(strings.NewReader(methoddata)) @@ -59,3 +79,50 @@ func TestMethodString(t *testing.T) { } } } + +func TestMethodSig(t *testing.T) { + var cases = []struct { + method string + expect string + }{ + { + method: "balance", + expect: "balance()", + }, + { + method: "send", + expect: "send(uint256)", + }, + { + method: "transfer", + expect: "transfer(address,address,uint256)", + }, + { + method: "tuple", + expect: "tuple((uint256,uint256))", + }, + { + method: "tupleArray", + expect: "tupleArray((uint256,uint256)[5])", + }, + { + method: "tupleSlice", + expect: "tupleSlice((uint256,uint256)[])", + }, + { + method: "complexTuple", + expect: "complexTuple((uint256,uint256)[5][])", + }, + } + abi, err := JSON(strings.NewReader(methoddata)) + if err != nil { + t.Fatal(err) + } + + for _, test := range cases { + got := abi.Methods[test.method].Sig() + if got != test.expect { + t.Errorf("expected string to be %s, got %s", test.expect, got) + } + } +} diff --git a/accounts/abi/reflect.go b/accounts/abi/reflect.go index fc2faa6869..83e4059d2d 100644 --- a/accounts/abi/reflect.go +++ b/accounts/abi/reflect.go @@ -31,6 +31,14 @@ func indirect(v reflect.Value) reflect.Value { return v } +// indirectInterfaceOrPtr recursively dereferences the value until value is not interface. +func indirectInterfaceOrPtr(v reflect.Value) reflect.Value { + if (v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr) && v.Elem().IsValid() { + return indirect(v.Elem()) + } + return v +} + // reflectIntKind returns the reflect using the given size and // unsignedness. func reflectIntKindAndType(unsigned bool, size int) (reflect.Kind, reflect.Type) { diff --git a/accounts/abi/type.go b/accounts/abi/type.go index 90ebb75ab2..0b15dcc401 100644 --- a/accounts/abi/type.go +++ b/accounts/abi/type.go @@ -68,7 +68,6 @@ func NewType(t string, components []ArgumentMarshaling) (typ Type, err error) { if strings.Count(t, "[") != strings.Count(t, "]") { return Type{}, errors.New("invalid arg type in abi") } - typ.stringKind = t // if there are brackets, get ready to go into slice/array mode and @@ -92,9 +91,7 @@ func NewType(t string, components []ArgumentMarshaling) (typ Type, err error) { typ.Kind = reflect.Slice typ.Elem = &embeddedType typ.Type = reflect.SliceOf(embeddedType.Type) - if embeddedType.T == TupleTy { - typ.stringKind = embeddedType.stringKind + sliced - } + typ.stringKind = embeddedType.stringKind + sliced } else if len(intz) == 1 { // is a array typ.T = ArrayTy @@ -105,9 +102,7 @@ func NewType(t string, components []ArgumentMarshaling) (typ Type, err error) { return Type{}, fmt.Errorf("abi: error parsing variable size: %v", err) } typ.Type = reflect.ArrayOf(typ.Size, embeddedType.Type) - if embeddedType.T == TupleTy { - typ.stringKind = embeddedType.stringKind + sliced - } + typ.stringKind = embeddedType.stringKind + sliced } else { return Type{}, errors.New("invalid formatting of array type") } diff --git a/accounts/abi/unpack_test.go b/accounts/abi/unpack_test.go index e8f8995f5c..391927dc8b 100644 --- a/accounts/abi/unpack_test.go +++ b/accounts/abi/unpack_test.go @@ -965,25 +965,21 @@ func TestUnpackTuple(t *testing.T) { buff.Write(common.Hex2Bytes("0000000000000000000000000000000000000000000000000000000000000001")) // ret[a] = 1 buff.Write(common.Hex2Bytes("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")) // ret[b] = -1 + // If the result is single tuple, use struct as return value container directly. v := struct { - Ret struct { - A *big.Int - B *big.Int - } - }{Ret: struct { A *big.Int B *big.Int - }{new(big.Int), new(big.Int)}} + }{new(big.Int), new(big.Int)} err = abi.Unpack(&v, "tuple", buff.Bytes()) if err != nil { t.Error(err) } else { - if v.Ret.A.Cmp(big.NewInt(1)) != 0 { - t.Errorf("unexpected value unpacked: want %x, got %x", 1, v.Ret.A) + if v.A.Cmp(big.NewInt(1)) != 0 { + t.Errorf("unexpected value unpacked: want %x, got %x", 1, v.A) } - if v.Ret.B.Cmp(big.NewInt(-1)) != 0 { - t.Errorf("unexpected value unpacked: want %x, got %x", v.Ret.B, -1) + if v.B.Cmp(big.NewInt(-1)) != 0 { + t.Errorf("unexpected value unpacked: want %x, got %x", v.B, -1) } }