github.com/sbrajchuk/go-ethereum@v1.9.7/accounts/abi/bind/bind.go (about)

     1  // Copyright 2016 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  // Package bind generates Ethereum contract Go bindings.
    18  //
    19  // Detailed usage document and tutorial available on the go-ethereum Wiki page:
    20  // https://github.com/ethereum/go-ethereum/wiki/Native-DApps:-Go-bindings-to-Ethereum-contracts
    21  package bind
    22  
    23  import (
    24  	"bytes"
    25  	"errors"
    26  	"fmt"
    27  	"go/format"
    28  	"regexp"
    29  	"strings"
    30  	"text/template"
    31  	"unicode"
    32  
    33  	"github.com/ethereum/go-ethereum/accounts/abi"
    34  	"github.com/ethereum/go-ethereum/log"
    35  )
    36  
    37  // Lang is a target programming language selector to generate bindings for.
    38  type Lang int
    39  
    40  const (
    41  	LangGo Lang = iota
    42  	LangJava
    43  	LangObjC
    44  )
    45  
    46  // Bind generates a Go wrapper around a contract ABI. This wrapper isn't meant
    47  // to be used as is in client code, but rather as an intermediate struct which
    48  // enforces compile time type safety and naming convention opposed to having to
    49  // manually maintain hard coded strings that break on runtime.
    50  func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, lang Lang, libs map[string]string) (string, error) {
    51  	// Process each individual contract requested binding
    52  	contracts := make(map[string]*tmplContract)
    53  
    54  	// Map used to flag each encountered library as such
    55  	isLib := make(map[string]struct{})
    56  
    57  	for i := 0; i < len(types); i++ {
    58  		// Parse the actual ABI to generate the binding for
    59  		evmABI, err := abi.JSON(strings.NewReader(abis[i]))
    60  		if err != nil {
    61  			return "", err
    62  		}
    63  		// Strip any whitespace from the JSON ABI
    64  		strippedABI := strings.Map(func(r rune) rune {
    65  			if unicode.IsSpace(r) {
    66  				return -1
    67  			}
    68  			return r
    69  		}, abis[i])
    70  
    71  		// Extract the call and transact methods; events, struct definitions; and sort them alphabetically
    72  		var (
    73  			calls     = make(map[string]*tmplMethod)
    74  			transacts = make(map[string]*tmplMethod)
    75  			events    = make(map[string]*tmplEvent)
    76  			structs   = make(map[string]*tmplStruct)
    77  		)
    78  		for _, original := range evmABI.Methods {
    79  			// Normalize the method for capital cases and non-anonymous inputs/outputs
    80  			normalized := original
    81  			normalized.Name = methodNormalizer[lang](original.Name)
    82  
    83  			normalized.Inputs = make([]abi.Argument, len(original.Inputs))
    84  			copy(normalized.Inputs, original.Inputs)
    85  			for j, input := range normalized.Inputs {
    86  				if input.Name == "" {
    87  					normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j)
    88  				}
    89  				if hasStruct(input.Type) {
    90  					bindStructType[lang](input.Type, structs)
    91  				}
    92  			}
    93  			normalized.Outputs = make([]abi.Argument, len(original.Outputs))
    94  			copy(normalized.Outputs, original.Outputs)
    95  			for j, output := range normalized.Outputs {
    96  				if output.Name != "" {
    97  					normalized.Outputs[j].Name = capitalise(output.Name)
    98  				}
    99  				if hasStruct(output.Type) {
   100  					bindStructType[lang](output.Type, structs)
   101  				}
   102  			}
   103  			// Append the methods to the call or transact lists
   104  			if original.Const {
   105  				calls[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)}
   106  			} else {
   107  				transacts[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)}
   108  			}
   109  		}
   110  		for _, original := range evmABI.Events {
   111  			// Skip anonymous events as they don't support explicit filtering
   112  			if original.Anonymous {
   113  				continue
   114  			}
   115  			// Normalize the event for capital cases and non-anonymous outputs
   116  			normalized := original
   117  			normalized.Name = methodNormalizer[lang](original.Name)
   118  
   119  			normalized.Inputs = make([]abi.Argument, len(original.Inputs))
   120  			copy(normalized.Inputs, original.Inputs)
   121  			for j, input := range normalized.Inputs {
   122  				if input.Name == "" {
   123  					normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j)
   124  				}
   125  				if hasStruct(input.Type) {
   126  					bindStructType[lang](input.Type, structs)
   127  				}
   128  			}
   129  			// Append the event to the accumulator list
   130  			events[original.Name] = &tmplEvent{Original: original, Normalized: normalized}
   131  		}
   132  
   133  		// There is no easy way to pass arbitrary java objects to the Go side.
   134  		if len(structs) > 0 && lang == LangJava {
   135  			return "", errors.New("java binding for tuple arguments is not supported yet")
   136  		}
   137  
   138  		contracts[types[i]] = &tmplContract{
   139  			Type:        capitalise(types[i]),
   140  			InputABI:    strings.Replace(strippedABI, "\"", "\\\"", -1),
   141  			InputBin:    strings.TrimPrefix(strings.TrimSpace(bytecodes[i]), "0x"),
   142  			Constructor: evmABI.Constructor,
   143  			Calls:       calls,
   144  			Transacts:   transacts,
   145  			Events:      events,
   146  			Libraries:   make(map[string]string),
   147  			Structs:     structs,
   148  		}
   149  		// Function 4-byte signatures are stored in the same sequence
   150  		// as types, if available.
   151  		if len(fsigs) > i {
   152  			contracts[types[i]].FuncSigs = fsigs[i]
   153  		}
   154  		// Parse library references.
   155  		for pattern, name := range libs {
   156  			matched, err := regexp.Match("__\\$"+pattern+"\\$__", []byte(contracts[types[i]].InputBin))
   157  			if err != nil {
   158  				log.Error("Could not search for pattern", "pattern", pattern, "contract", contracts[types[i]], "err", err)
   159  			}
   160  			if matched {
   161  				contracts[types[i]].Libraries[pattern] = name
   162  				// keep track that this type is a library
   163  				if _, ok := isLib[name]; !ok {
   164  					isLib[name] = struct{}{}
   165  				}
   166  			}
   167  		}
   168  	}
   169  	// Check if that type has already been identified as a library
   170  	for i := 0; i < len(types); i++ {
   171  		_, ok := isLib[types[i]]
   172  		contracts[types[i]].Library = ok
   173  	}
   174  	// Generate the contract template data content and render it
   175  	data := &tmplData{
   176  		Package:   pkg,
   177  		Contracts: contracts,
   178  		Libraries: libs,
   179  	}
   180  	buffer := new(bytes.Buffer)
   181  
   182  	funcs := map[string]interface{}{
   183  		"bindtype":      bindType[lang],
   184  		"bindtopictype": bindTopicType[lang],
   185  		"namedtype":     namedType[lang],
   186  		"formatmethod":  formatMethod,
   187  		"formatevent":   formatEvent,
   188  		"capitalise":    capitalise,
   189  		"decapitalise":  decapitalise,
   190  	}
   191  	tmpl := template.Must(template.New("").Funcs(funcs).Parse(tmplSource[lang]))
   192  	if err := tmpl.Execute(buffer, data); err != nil {
   193  		return "", err
   194  	}
   195  	// For Go bindings pass the code through gofmt to clean it up
   196  	if lang == LangGo {
   197  		code, err := format.Source(buffer.Bytes())
   198  		if err != nil {
   199  			return "", fmt.Errorf("%v\n%s", err, buffer)
   200  		}
   201  		return string(code), nil
   202  	}
   203  	// For all others just return as is for now
   204  	return buffer.String(), nil
   205  }
   206  
   207  // bindType is a set of type binders that convert Solidity types to some supported
   208  // programming language types.
   209  var bindType = map[Lang]func(kind abi.Type, structs map[string]*tmplStruct) string{
   210  	LangGo:   bindTypeGo,
   211  	LangJava: bindTypeJava,
   212  }
   213  
   214  // bindBasicTypeGo converts basic solidity types(except array, slice and tuple) to Go one.
   215  func bindBasicTypeGo(kind abi.Type) string {
   216  	switch kind.T {
   217  	case abi.AddressTy:
   218  		return "common.Address"
   219  	case abi.IntTy, abi.UintTy:
   220  		parts := regexp.MustCompile(`(u)?int([0-9]*)`).FindStringSubmatch(kind.String())
   221  		switch parts[2] {
   222  		case "8", "16", "32", "64":
   223  			return fmt.Sprintf("%sint%s", parts[1], parts[2])
   224  		}
   225  		return "*big.Int"
   226  	case abi.FixedBytesTy:
   227  		return fmt.Sprintf("[%d]byte", kind.Size)
   228  	case abi.BytesTy:
   229  		return "[]byte"
   230  	case abi.FunctionTy:
   231  		return "[24]byte"
   232  	default:
   233  		// string, bool types
   234  		return kind.String()
   235  	}
   236  }
   237  
   238  // bindTypeGo converts solidity types to Go ones. Since there is no clear mapping
   239  // from all Solidity types to Go ones (e.g. uint17), those that cannot be exactly
   240  // mapped will use an upscaled type (e.g. BigDecimal).
   241  func bindTypeGo(kind abi.Type, structs map[string]*tmplStruct) string {
   242  	switch kind.T {
   243  	case abi.TupleTy:
   244  		return structs[kind.TupleRawName+kind.String()].Name
   245  	case abi.ArrayTy:
   246  		return fmt.Sprintf("[%d]", kind.Size) + bindTypeGo(*kind.Elem, structs)
   247  	case abi.SliceTy:
   248  		return "[]" + bindTypeGo(*kind.Elem, structs)
   249  	default:
   250  		return bindBasicTypeGo(kind)
   251  	}
   252  }
   253  
   254  // bindBasicTypeJava converts basic solidity types(except array, slice and tuple) to Java one.
   255  func bindBasicTypeJava(kind abi.Type) string {
   256  	switch kind.T {
   257  	case abi.AddressTy:
   258  		return "Address"
   259  	case abi.IntTy, abi.UintTy:
   260  		// Note that uint and int (without digits) are also matched,
   261  		// these are size 256, and will translate to BigInt (the default).
   262  		parts := regexp.MustCompile(`(u)?int([0-9]*)`).FindStringSubmatch(kind.String())
   263  		if len(parts) != 3 {
   264  			return kind.String()
   265  		}
   266  		// All unsigned integers should be translated to BigInt since gomobile doesn't
   267  		// support them.
   268  		if parts[1] == "u" {
   269  			return "BigInt"
   270  		}
   271  
   272  		namedSize := map[string]string{
   273  			"8":  "byte",
   274  			"16": "short",
   275  			"32": "int",
   276  			"64": "long",
   277  		}[parts[2]]
   278  
   279  		// default to BigInt
   280  		if namedSize == "" {
   281  			namedSize = "BigInt"
   282  		}
   283  		return namedSize
   284  	case abi.FixedBytesTy, abi.BytesTy:
   285  		return "byte[]"
   286  	case abi.BoolTy:
   287  		return "boolean"
   288  	case abi.StringTy:
   289  		return "String"
   290  	case abi.FunctionTy:
   291  		return "byte[24]"
   292  	default:
   293  		return kind.String()
   294  	}
   295  }
   296  
   297  // pluralizeJavaType explicitly converts multidimensional types to predefined
   298  // type in go side.
   299  func pluralizeJavaType(typ string) string {
   300  	switch typ {
   301  	case "boolean":
   302  		return "Bools"
   303  	case "String":
   304  		return "Strings"
   305  	case "Address":
   306  		return "Addresses"
   307  	case "byte[]":
   308  		return "Binaries"
   309  	case "BigInt":
   310  		return "BigInts"
   311  	}
   312  	return typ + "[]"
   313  }
   314  
   315  // bindTypeJava converts a Solidity type to a Java one. Since there is no clear mapping
   316  // from all Solidity types to Java ones (e.g. uint17), those that cannot be exactly
   317  // mapped will use an upscaled type (e.g. BigDecimal).
   318  func bindTypeJava(kind abi.Type, structs map[string]*tmplStruct) string {
   319  	switch kind.T {
   320  	case abi.TupleTy:
   321  		return structs[kind.TupleRawName+kind.String()].Name
   322  	case abi.ArrayTy, abi.SliceTy:
   323  		return pluralizeJavaType(bindTypeJava(*kind.Elem, structs))
   324  	default:
   325  		return bindBasicTypeJava(kind)
   326  	}
   327  }
   328  
   329  // bindTopicType is a set of type binders that convert Solidity types to some
   330  // supported programming language topic types.
   331  var bindTopicType = map[Lang]func(kind abi.Type, structs map[string]*tmplStruct) string{
   332  	LangGo:   bindTopicTypeGo,
   333  	LangJava: bindTopicTypeJava,
   334  }
   335  
   336  // bindTopicTypeGo converts a Solidity topic type to a Go one. It is almost the same
   337  // funcionality as for simple types, but dynamic types get converted to hashes.
   338  func bindTopicTypeGo(kind abi.Type, structs map[string]*tmplStruct) string {
   339  	bound := bindTypeGo(kind, structs)
   340  
   341  	// todo(rjl493456442) according solidity documentation, indexed event
   342  	// parameters that are not value types i.e. arrays and structs are not
   343  	// stored directly but instead a keccak256-hash of an encoding is stored.
   344  	//
   345  	// We only convert stringS and bytes to hash, still need to deal with
   346  	// array(both fixed-size and dynamic-size) and struct.
   347  	if bound == "string" || bound == "[]byte" {
   348  		bound = "common.Hash"
   349  	}
   350  	return bound
   351  }
   352  
   353  // bindTopicTypeJava converts a Solidity topic type to a Java one. It is almost the same
   354  // funcionality as for simple types, but dynamic types get converted to hashes.
   355  func bindTopicTypeJava(kind abi.Type, structs map[string]*tmplStruct) string {
   356  	bound := bindTypeJava(kind, structs)
   357  
   358  	// todo(rjl493456442) according solidity documentation, indexed event
   359  	// parameters that are not value types i.e. arrays and structs are not
   360  	// stored directly but instead a keccak256-hash of an encoding is stored.
   361  	//
   362  	// We only convert stringS and bytes to hash, still need to deal with
   363  	// array(both fixed-size and dynamic-size) and struct.
   364  	if bound == "String" || bound == "byte[]" {
   365  		bound = "Hash"
   366  	}
   367  	return bound
   368  }
   369  
   370  // bindStructType is a set of type binders that convert Solidity tuple types to some supported
   371  // programming language struct definition.
   372  var bindStructType = map[Lang]func(kind abi.Type, structs map[string]*tmplStruct) string{
   373  	LangGo:   bindStructTypeGo,
   374  	LangJava: bindStructTypeJava,
   375  }
   376  
   377  // bindStructTypeGo converts a Solidity tuple type to a Go one and records the mapping
   378  // in the given map.
   379  // Notably, this function will resolve and record nested struct recursively.
   380  func bindStructTypeGo(kind abi.Type, structs map[string]*tmplStruct) string {
   381  	switch kind.T {
   382  	case abi.TupleTy:
   383  		// We compose raw struct name and canonical parameter expression
   384  		// together here. The reason is before solidity v0.5.11, kind.TupleRawName
   385  		// is empty, so we use canonical parameter expression to distinguish
   386  		// different struct definition. From the consideration of backward
   387  		// compatibility, we concat these two together so that if kind.TupleRawName
   388  		// is not empty, it can have unique id.
   389  		id := kind.TupleRawName + kind.String()
   390  		if s, exist := structs[id]; exist {
   391  			return s.Name
   392  		}
   393  		var fields []*tmplField
   394  		for i, elem := range kind.TupleElems {
   395  			field := bindStructTypeGo(*elem, structs)
   396  			fields = append(fields, &tmplField{Type: field, Name: capitalise(kind.TupleRawNames[i]), SolKind: *elem})
   397  		}
   398  		name := kind.TupleRawName
   399  		if name == "" {
   400  			name = fmt.Sprintf("Struct%d", len(structs))
   401  		}
   402  		structs[id] = &tmplStruct{
   403  			Name:   name,
   404  			Fields: fields,
   405  		}
   406  		return name
   407  	case abi.ArrayTy:
   408  		return fmt.Sprintf("[%d]", kind.Size) + bindStructTypeGo(*kind.Elem, structs)
   409  	case abi.SliceTy:
   410  		return "[]" + bindStructTypeGo(*kind.Elem, structs)
   411  	default:
   412  		return bindBasicTypeGo(kind)
   413  	}
   414  }
   415  
   416  // bindStructTypeJava converts a Solidity tuple type to a Java one and records the mapping
   417  // in the given map.
   418  // Notably, this function will resolve and record nested struct recursively.
   419  func bindStructTypeJava(kind abi.Type, structs map[string]*tmplStruct) string {
   420  	switch kind.T {
   421  	case abi.TupleTy:
   422  		// We compose raw struct name and canonical parameter expression
   423  		// together here. The reason is before solidity v0.5.11, kind.TupleRawName
   424  		// is empty, so we use canonical parameter expression to distinguish
   425  		// different struct definition. From the consideration of backward
   426  		// compatibility, we concat these two together so that if kind.TupleRawName
   427  		// is not empty, it can have unique id.
   428  		id := kind.TupleRawName + kind.String()
   429  		if s, exist := structs[id]; exist {
   430  			return s.Name
   431  		}
   432  		var fields []*tmplField
   433  		for i, elem := range kind.TupleElems {
   434  			field := bindStructTypeJava(*elem, structs)
   435  			fields = append(fields, &tmplField{Type: field, Name: decapitalise(kind.TupleRawNames[i]), SolKind: *elem})
   436  		}
   437  		name := kind.TupleRawName
   438  		if name == "" {
   439  			name = fmt.Sprintf("Class%d", len(structs))
   440  		}
   441  		structs[id] = &tmplStruct{
   442  			Name:   name,
   443  			Fields: fields,
   444  		}
   445  		return name
   446  	case abi.ArrayTy, abi.SliceTy:
   447  		return pluralizeJavaType(bindStructTypeJava(*kind.Elem, structs))
   448  	default:
   449  		return bindBasicTypeJava(kind)
   450  	}
   451  }
   452  
   453  // namedType is a set of functions that transform language specific types to
   454  // named versions that my be used inside method names.
   455  var namedType = map[Lang]func(string, abi.Type) string{
   456  	LangGo:   func(string, abi.Type) string { panic("this shouldn't be needed") },
   457  	LangJava: namedTypeJava,
   458  }
   459  
   460  // namedTypeJava converts some primitive data types to named variants that can
   461  // be used as parts of method names.
   462  func namedTypeJava(javaKind string, solKind abi.Type) string {
   463  	switch javaKind {
   464  	case "byte[]":
   465  		return "Binary"
   466  	case "boolean":
   467  		return "Bool"
   468  	default:
   469  		parts := regexp.MustCompile(`(u)?int([0-9]*)(\[[0-9]*\])?`).FindStringSubmatch(solKind.String())
   470  		if len(parts) != 4 {
   471  			return javaKind
   472  		}
   473  		switch parts[2] {
   474  		case "8", "16", "32", "64":
   475  			if parts[3] == "" {
   476  				return capitalise(fmt.Sprintf("%sint%s", parts[1], parts[2]))
   477  			}
   478  			return capitalise(fmt.Sprintf("%sint%ss", parts[1], parts[2]))
   479  
   480  		default:
   481  			return javaKind
   482  		}
   483  	}
   484  }
   485  
   486  // methodNormalizer is a name transformer that modifies Solidity method names to
   487  // conform to target language naming concentions.
   488  var methodNormalizer = map[Lang]func(string) string{
   489  	LangGo:   abi.ToCamelCase,
   490  	LangJava: decapitalise,
   491  }
   492  
   493  // capitalise makes a camel-case string which starts with an upper case character.
   494  func capitalise(input string) string {
   495  	return abi.ToCamelCase(input)
   496  }
   497  
   498  // decapitalise makes a camel-case string which starts with a lower case character.
   499  func decapitalise(input string) string {
   500  	if len(input) == 0 {
   501  		return input
   502  	}
   503  
   504  	goForm := abi.ToCamelCase(input)
   505  	return strings.ToLower(goForm[:1]) + goForm[1:]
   506  }
   507  
   508  // structured checks whether a list of ABI data types has enough information to
   509  // operate through a proper Go struct or if flat returns are needed.
   510  func structured(args abi.Arguments) bool {
   511  	if len(args) < 2 {
   512  		return false
   513  	}
   514  	exists := make(map[string]bool)
   515  	for _, out := range args {
   516  		// If the name is anonymous, we can't organize into a struct
   517  		if out.Name == "" {
   518  			return false
   519  		}
   520  		// If the field name is empty when normalized or collides (var, Var, _var, _Var),
   521  		// we can't organize into a struct
   522  		field := capitalise(out.Name)
   523  		if field == "" || exists[field] {
   524  			return false
   525  		}
   526  		exists[field] = true
   527  	}
   528  	return true
   529  }
   530  
   531  // hasStruct returns an indicator whether the given type is struct, struct slice
   532  // or struct array.
   533  func hasStruct(t abi.Type) bool {
   534  	switch t.T {
   535  	case abi.SliceTy:
   536  		return hasStruct(*t.Elem)
   537  	case abi.ArrayTy:
   538  		return hasStruct(*t.Elem)
   539  	case abi.TupleTy:
   540  		return true
   541  	default:
   542  		return false
   543  	}
   544  }
   545  
   546  // resolveArgName converts a raw argument representation into a user friendly format.
   547  func resolveArgName(arg abi.Argument, structs map[string]*tmplStruct) string {
   548  	var (
   549  		prefix   string
   550  		embedded string
   551  		typ      = &arg.Type
   552  	)
   553  loop:
   554  	for {
   555  		switch typ.T {
   556  		case abi.SliceTy:
   557  			prefix += "[]"
   558  		case abi.ArrayTy:
   559  			prefix += fmt.Sprintf("[%d]", typ.Size)
   560  		default:
   561  			embedded = typ.TupleRawName + typ.String()
   562  			break loop
   563  		}
   564  		typ = typ.Elem
   565  	}
   566  	if s, exist := structs[embedded]; exist {
   567  		return prefix + s.Name
   568  	} else {
   569  		return arg.Type.String()
   570  	}
   571  }
   572  
   573  // formatMethod transforms raw method representation into a user friendly one.
   574  func formatMethod(method abi.Method, structs map[string]*tmplStruct) string {
   575  	inputs := make([]string, len(method.Inputs))
   576  	for i, input := range method.Inputs {
   577  		inputs[i] = fmt.Sprintf("%v %v", resolveArgName(input, structs), input.Name)
   578  	}
   579  	outputs := make([]string, len(method.Outputs))
   580  	for i, output := range method.Outputs {
   581  		outputs[i] = resolveArgName(output, structs)
   582  		if len(output.Name) > 0 {
   583  			outputs[i] += fmt.Sprintf(" %v", output.Name)
   584  		}
   585  	}
   586  	constant := ""
   587  	if method.Const {
   588  		constant = "constant "
   589  	}
   590  	return fmt.Sprintf("function %v(%v) %sreturns(%v)", method.RawName, strings.Join(inputs, ", "), constant, strings.Join(outputs, ", "))
   591  }
   592  
   593  // formatEvent transforms raw event representation into a user friendly one.
   594  func formatEvent(event abi.Event, structs map[string]*tmplStruct) string {
   595  	inputs := make([]string, len(event.Inputs))
   596  	for i, input := range event.Inputs {
   597  		if input.Indexed {
   598  			inputs[i] = fmt.Sprintf("%v indexed %v", resolveArgName(input, structs), input.Name)
   599  		} else {
   600  			inputs[i] = fmt.Sprintf("%v %v", resolveArgName(input, structs), input.Name)
   601  		}
   602  	}
   603  	return fmt.Sprintf("event %v(%v)", event.RawName, strings.Join(inputs, ", "))
   604  }