From b6d6ba92decb7e9c46b8dbccf2bfd263edf8b5c9 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 17 Dec 2024 19:57:03 +0700 Subject: [PATCH] refactor builds --- accounts/abi/bind/bind.go | 361 ++++++++++++++---- accounts/abi/bind/template.go | 9 +- .../bind/v2/internal/solc_errors/contract.sol | 2 +- cmd/abigen/main.go | 2 +- 4 files changed, 292 insertions(+), 82 deletions(-) diff --git a/accounts/abi/bind/bind.go b/accounts/abi/bind/bind.go index d1e0a39a32..6ab327b60a 100644 --- a/accounts/abi/bind/bind.go +++ b/accounts/abi/bind/bind.go @@ -75,9 +75,250 @@ func isKeyWord(arg string) bool { // enforces compile time type safety and naming convention as opposed to having to // manually maintain hard coded strings that break on runtime. func Bind(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, libs map[string]string, aliases map[string]string) (string, error) { - data, err := bind(types, abis, bytecodes, fsigs, pkg, libs, aliases) - if err != nil { - return "", err + var ( + // contracts is the map of each individual contract requested binding + contracts = make(map[string]*tmplContract) + + // structs is the map of all redeclared structs shared by passed contracts. + structs = make(map[string]*tmplStruct) + + // isLib is the map used to flag each encountered library as such + isLib = make(map[string]struct{}) + ) + for i := 0; i < len(types); i++ { + // Parse the actual ABI to generate the binding for + evmABI, err := abi.JSON(strings.NewReader(abis[i])) + if err != nil { + return "", err + } + // Strip any whitespace from the JSON ABI + strippedABI := strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 + } + return r + }, abis[i]) + + // 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) + errors = make(map[string]*tmplError) + fallback *tmplMethod + receive *tmplMethod + + // identifiers are used to detect duplicated identifiers of functions + // and events. For all calls, transacts and events, abigen will generate + // corresponding bindings. However we have to ensure there is no + // identifier collisions in the bindings of these categories. + callIdentifiers = make(map[string]bool) + transactIdentifiers = make(map[string]bool) + eventIdentifiers = make(map[string]bool) + ) + + for _, input := range evmABI.Constructor.Inputs { + if hasStruct(input.Type) { + bindStructType(input.Type, structs) + } + } + + for _, original := range evmABI.Methods { + // Normalize the method for capital cases and non-anonymous inputs/outputs + normalized := original + normalizedName := methodNormalizer(alias(aliases, original.Name)) + // Ensure there is no duplicated identifier + var identifiers = callIdentifiers + if !original.IsConstant() { + identifiers = transactIdentifiers + } + // Name shouldn't start with a digit. It will make the generated code invalid. + if len(normalizedName) > 0 && unicode.IsDigit(rune(normalizedName[0])) { + normalizedName = fmt.Sprintf("M%s", normalizedName) + normalizedName = abi.ResolveNameConflict(normalizedName, func(name string) bool { + _, ok := identifiers[name] + return ok + }) + } + if identifiers[normalizedName] { + return "", fmt.Errorf("duplicated identifier \"%s\"(normalized \"%s\"), use --alias for renaming", original.Name, normalizedName) + } + identifiers[normalizedName] = true + + normalized.Name = normalizedName + normalized.Inputs = make([]abi.Argument, len(original.Inputs)) + copy(normalized.Inputs, original.Inputs) + for j, input := range normalized.Inputs { + if input.Name == "" || isKeyWord(input.Name) { + normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j) + } + if hasStruct(input.Type) { + bindStructType(input.Type, structs) + } + } + normalized.Outputs = make([]abi.Argument, len(original.Outputs)) + copy(normalized.Outputs, original.Outputs) + for j, output := range normalized.Outputs { + if output.Name != "" { + normalized.Outputs[j].Name = capitalise(output.Name) + } + if hasStruct(output.Type) { + bindStructType(output.Type, structs) + } + } + // Append the methods to the call or transact lists + if original.IsConstant() { + calls[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)} + } else { + transacts[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)} + } + } + for _, original := range evmABI.Events { + // Skip anonymous events as they don't support explicit filtering + if original.Anonymous { + continue + } + // Normalize the event for capital cases and non-anonymous outputs + normalized := original + + // Ensure there is no duplicated identifier + normalizedName := methodNormalizer(alias(aliases, original.Name)) + // Name shouldn't start with a digit. It will make the generated code invalid. + if len(normalizedName) > 0 && unicode.IsDigit(rune(normalizedName[0])) { + normalizedName = fmt.Sprintf("E%s", normalizedName) + normalizedName = abi.ResolveNameConflict(normalizedName, func(name string) bool { + _, ok := eventIdentifiers[name] + return ok + }) + } + if eventIdentifiers[normalizedName] { + return "", fmt.Errorf("duplicated identifier \"%s\"(normalized \"%s\"), use --alias for renaming", original.Name, normalizedName) + } + eventIdentifiers[normalizedName] = true + normalized.Name = normalizedName + + used := make(map[string]bool) + normalized.Inputs = make([]abi.Argument, len(original.Inputs)) + copy(normalized.Inputs, original.Inputs) + for j, input := range normalized.Inputs { + if input.Name == "" || isKeyWord(input.Name) { + normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j) + } + // Event is a bit special, we need to define event struct in binding, + // ensure there is no camel-case-style name conflict. + for index := 0; ; index++ { + if !used[capitalise(normalized.Inputs[j].Name)] { + used[capitalise(normalized.Inputs[j].Name)] = true + break + } + normalized.Inputs[j].Name = fmt.Sprintf("%s%d", normalized.Inputs[j].Name, index) + } + if hasStruct(input.Type) { + bindStructType(input.Type, structs) + } + } + // Append the event to the accumulator list + events[original.Name] = &tmplEvent{Original: original, Normalized: normalized} + } + for _, original := range evmABI.Errors { + // TODO: I copied this from events (above in this function). I think it should be correct but not totally sure + // even if it is correct, should consider deduplicating this into its own function. + + // Normalize the error for capital cases and non-anonymous outputs + normalized := original + + // Ensure there is no duplicated identifier + normalizedName := methodNormalizer(alias(aliases, original.Name)) + // Name shouldn't start with a digit. It will make the generated code invalid. + if len(normalizedName) > 0 && unicode.IsDigit(rune(normalizedName[0])) { + normalizedName = fmt.Sprintf("E%s", normalizedName) + normalizedName = abi.ResolveNameConflict(normalizedName, func(name string) bool { + _, ok := eventIdentifiers[name] + return ok + }) + } + if eventIdentifiers[normalizedName] { + return "", fmt.Errorf("duplicated identifier \"%s\"(normalized \"%s\"), use --alias for renaming", original.Name, normalizedName) + } + eventIdentifiers[normalizedName] = true + normalized.Name = normalizedName + + used := make(map[string]bool) + normalized.Inputs = make([]abi.Argument, len(original.Inputs)) + copy(normalized.Inputs, original.Inputs) + for j, input := range normalized.Inputs { + if input.Name == "" || isKeyWord(input.Name) { + normalized.Inputs[j].Name = fmt.Sprintf("arg%d", j) + } + // Event is a bit special, we need to define event struct in binding, + // ensure there is no camel-case-style name conflict. + for index := 0; ; index++ { + if !used[capitalise(normalized.Inputs[j].Name)] { + used[capitalise(normalized.Inputs[j].Name)] = true + break + } + normalized.Inputs[j].Name = fmt.Sprintf("%s%d", normalized.Inputs[j].Name, index) + } + if hasStruct(input.Type) { + bindStructType(input.Type, structs) + } + } + errors[original.Name] = &tmplError{Original: original, Normalized: normalized} + } + // Add two special fallback functions if they exist + if evmABI.HasFallback() { + fallback = &tmplMethod{Original: evmABI.Fallback} + } + if evmABI.HasReceive() { + receive = &tmplMethod{Original: evmABI.Receive} + } + + contracts[types[i]] = &tmplContract{ + Type: capitalise(types[i]), + InputABI: strings.ReplaceAll(strippedABI, "\"", "\\\""), + InputBin: strings.TrimPrefix(strings.TrimSpace(bytecodes[i]), "0x"), + Constructor: evmABI.Constructor, + Calls: calls, + Transacts: transacts, + Fallback: fallback, + Receive: receive, + Events: events, + Libraries: make(map[string]string), + AllLibraries: make(map[string]string), + } + + // Function 4-byte signatures are stored in the same sequence + // as types, if available. + if len(fsigs) > i { + contracts[types[i]].FuncSigs = fsigs[i] + } + // Parse library references. + for pattern, name := range libs { + matched, err := regexp.MatchString("__\\$"+pattern+"\\$__", contracts[types[i]].InputBin) + if err != nil { + log.Error("Could not search for pattern", "pattern", pattern, "contract", contracts[types[i]], "err", err) + } + if matched { + contracts[types[i]].Libraries[pattern] = name + // keep track that this type is a library + if _, ok := isLib[name]; !ok { + isLib[name] = struct{}{} + } + } + } + } + // Check if that type has already been identified as a library + for i := 0; i < len(types); i++ { + _, ok := isLib[types[i]] + contracts[types[i]].Library = ok + } + + // Generate the contract template data content and render it + data := &tmplData{ + Package: pkg, + Contracts: contracts, + Libraries: libs, + Structs: structs, } buffer := new(bytes.Buffer) @@ -106,9 +347,6 @@ type binder struct { // structs is the map of all redeclared structs shared by passed contracts. structs map[string]*tmplStruct - // isLib is the map used to flag each encountered library as such - isLib map[string]struct{} - // identifiers are used to detect duplicated identifiers of functions // and events. For all calls, transacts and events, abigen will generate // corresponding bindings. However we have to ensure there is no @@ -194,8 +432,29 @@ func (cb *contractBinder) bindMethod(original abi.Method) error { cb.binder.BindStructType(output.Type) } } + isStructured := structured(original.Outputs) + // if the call returns multiple values, coallesce them into a struct + if len(normalized.Outputs) > 1 { + // Build up dictionary of existing arg names. + keys := make(map[string]struct{}) + for _, o := range normalized.Outputs { + if o.Name != "" { + keys[strings.ToLower(o.Name)] = struct{}{} + } + } + // Assign names to anonymous fields. + for i, o := range normalized.Outputs { + if o.Name != "" { + continue + } + o.Name = capitalise(abi.ResolveNameConflict("arg", func(name string) bool { _, ok := keys[name]; return ok })) + normalized.Outputs[i] = o + keys[strings.ToLower(o.Name)] = struct{}{} + } + isStructured = true + } - cb.calls[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: structured(original.Outputs)} + cb.calls[original.Name] = &tmplMethod{Original: original, Normalized: normalized, Structured: isStructured} return nil } @@ -251,16 +510,23 @@ func (cb *contractBinder) bindError(original abi.Error) error { return nil } -func BindV22(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, libs map[string]string, aliases map[string]string) (string, error) { +func BindV2(types []string, abis []string, bytecodes []string, pkg string, libs map[string]string, aliases map[string]string) (string, error) { // TODO: validate each alias (ensure it doesn't begin with a digit or other invalid character) - b := binder{} + b := binder{ + contracts: make(map[string]*tmplContractV2), + structs: make(map[string]*tmplStruct), + callIdentifiers: nil, + eventIdentifiers: nil, + errorIdentifiers: nil, + aliases: nil, + } for i := 0; i < len(types); i++ { // Parse the actual ABI to generate the binding for evmABI, err := abi.JSON(strings.NewReader(abis[i])) if err != nil { - return nil, err + return "", err } // Strip any whitespace from the JSON ABI strippedABI := strings.Map(func(r rune) rune { @@ -279,18 +545,18 @@ func BindV22(types []string, abis []string, bytecodes []string, fsigs []map[stri cb := contractBinder{} for _, original := range evmABI.Methods { if err := cb.bindMethod(original); err != nil { - // TODO: do something bad here... + return "", err } } for _, original := range evmABI.Events { if err := cb.bindEvent(original); err != nil { - // TODO: do something bad here... + return "", err } } for _, original := range evmABI.Errors { if err := cb.bindError(original); err != nil { - // TODO: do something bad here... + return "", err } } @@ -307,72 +573,18 @@ func BindV22(types []string, abis []string, bytecodes []string, fsigs []map[stri } } - invertedLibs := make(map[string]string) // map of pattern -> unlinked bytecode + invertedLibs := make(map[string]string) for pattern, name := range libs { invertedLibs[name] = pattern } data := tmplDataV2{ - Package: pkg, - Contracts: b.contracts, - InvertedLibs: invertedLibs, - Libraries: b - Structs: b.structs, + Package: pkg, + Contracts: b.contracts, + Libraries: invertedLibs, + Structs: b.structs, } -} - -func BindV2(types []string, abis []string, bytecodes []string, fsigs []map[string]string, pkg string, libs map[string]string, aliases map[string]string) (string, error) { - data, err := bind(types, abis, bytecodes, fsigs, pkg, libs, aliases) - - if err != nil { - return "", err - } - for _, c := range data.Contracts { - // We want pack/unpack methods for all existing methods. - for name, t := range c.Transacts { - c.Calls[name] = t - } - c.Transacts = nil - - // Make sure we return one argument. If multiple exist - // merge them into a struct. - for _, call := range c.Calls { - if call.Structured { - continue - } - if len(call.Normalized.Outputs) < 2 { - continue - } - // Build up dictionary of existing arg names. - keys := make(map[string]struct{}) - for _, o := range call.Normalized.Outputs { - if o.Name != "" { - keys[strings.ToLower(o.Name)] = struct{}{} - } - } - // Assign names to anonymous fields. - for i, o := range call.Normalized.Outputs { - if o.Name != "" { - continue - } - o.Name = capitalise(abi.ResolveNameConflict("arg", func(name string) bool { _, ok := keys[name]; return ok })) - call.Normalized.Outputs[i] = o - keys[strings.ToLower(o.Name)] = struct{}{} - } - call.Structured = true - } - } - - // map of contract name -> pattern - invertedLibs := make(map[string]string) - // assume that this is invertible/onto because I assume library names are unique now - // TODO: check that they've been sanitized at this point. - for pattern, name := range libs { - invertedLibs[name] = pattern - } - data.InvertedLibs = invertedLibs - contractsBins := make(map[string]string) for typ, contract := range data.Contracts { pattern := invertedLibs[typ] @@ -385,13 +597,13 @@ func BindV2(types []string, abis []string, bytecodes []string, fsigs []map[strin contractType := libs[dep.pattern] for subDepPattern, _ := range dep.Flatten() { if subDepPattern == dep.pattern { + // don't include the dep as a dependency of itself continue } subDepType := libs[subDepPattern] - data.Contracts[contractType].AllLibraries[subDepType] = subDepPattern + data.Contracts[contractType].Libraries[subDepType] = subDepPattern } } - buffer := new(bytes.Buffer) funcs := map[string]interface{}{ "bindtype": bindType, @@ -625,7 +837,6 @@ func bind(types []string, abis []string, bytecodes []string, fsigs []map[string] Events: events, Libraries: make(map[string]string), AllLibraries: make(map[string]string), - Errors: errors, } // Function 4-byte signatures are stored in the same sequence diff --git a/accounts/abi/bind/template.go b/accounts/abi/bind/template.go index d526dcbb4d..79051a5d66 100644 --- a/accounts/abi/bind/template.go +++ b/accounts/abi/bind/template.go @@ -60,11 +60,10 @@ type tmplContractV2 struct { } type tmplDataV2 struct { - Package string // Name of the package to place the generated file in - Contracts map[string]*tmplContractV2 // List of contracts to generate into this file - Libraries map[string]string // Map the bytecode's link pattern to the library name - InvertedLibs map[string]string // map of the contract's name to the link pattern - Structs map[string]*tmplStruct // Contract struct type definitions + Package string // Name of the package to place the generated file in + Contracts map[string]*tmplContractV2 // List of contracts to generate into this file + Libraries map[string]string // Map of the contract's name to link pattern + Structs map[string]*tmplStruct // Contract struct type definitions } // tmplMethod is a wrapper around an abi.Method that contains a few preprocessed diff --git a/accounts/abi/bind/v2/internal/solc_errors/contract.sol b/accounts/abi/bind/v2/internal/solc_errors/contract.sol index 541352a1d8..02c90e76ef 100644 --- a/accounts/abi/bind/v2/internal/solc_errors/contract.sol +++ b/accounts/abi/bind/v2/internal/solc_errors/contract.sol @@ -24,7 +24,7 @@ contract C { } // purpose of this is to test that generation of metadata for contract that emits one error produces valid Go code -contract C2 { +contract 2C2 { function Foo() public pure { revert BadThing({ arg1: uint256(0), diff --git a/cmd/abigen/main.go b/cmd/abigen/main.go index c182c42ab2..d573176372 100644 --- a/cmd/abigen/main.go +++ b/cmd/abigen/main.go @@ -213,7 +213,7 @@ func abigen(c *cli.Context) error { err error ) if c.IsSet(v2Flag.Name) { - code, err = bind.BindV2(types, abis, bins, sigs, c.String(pkgFlag.Name), libs, aliases) + code, err = bind.BindV2(types, abis, bins, c.String(pkgFlag.Name), libs, aliases) } else { code, err = bind.Bind(types, abis, bins, sigs, c.String(pkgFlag.Name), libs, aliases) }