diff --git a/cmd/abigen/main.go b/cmd/abigen/main.go index 78d2fa3867663..687224cb75457 100644 --- a/cmd/abigen/main.go +++ b/cmd/abigen/main.go @@ -21,9 +21,11 @@ import ( "fmt" "io" "os" + "path/filepath" "regexp" "strings" + "github.com/blockcypher/go-ethereum/accounts/abi" "github.com/blockcypher/go-ethereum/accounts/abi/bind" "github.com/blockcypher/go-ethereum/cmd/utils" "github.com/blockcypher/go-ethereum/common/compiler" @@ -51,6 +53,24 @@ var ( Name: "combined-json", Usage: "Path to the combined-json file generated by compiler, - for STDIN", } + solFlag = &cli.StringFlag{ + Name: "sol", + Usage: "Path to the Ethereum contract Solidity source to build and bind", + } + solcFlag = &cli.StringFlag{ + Name: "solc", + Usage: "Solidity compiler to use if source builds are requested", + Value: "solc", + } + vyFlag = &cli.StringFlag{ + Name: "vy", + Usage: "Path to the Ethereum contract Vyper source to build and bind", + } + vyperFlag = &cli.StringFlag{ + Name: "vyper", + Usage: "Vyper compiler to use if source builds are requested", + Value: "vyper", + } excFlag = &cli.StringFlag{ Name: "exc", Usage: "Comma separated types to exclude from binding", @@ -83,6 +103,10 @@ func init() { binFlag, typeFlag, jsonFlag, + solFlag, + solcFlag, + vyFlag, + vyperFlag, excFlag, pkgFlag, outFlag, @@ -93,7 +117,7 @@ func init() { } func abigen(c *cli.Context) error { - utils.CheckExclusive(c, abiFlag, jsonFlag) // Only one source can be selected. + utils.CheckExclusive(c, abiFlag, jsonFlag, solFlag, vyFlag) // Only one source can be selected. if c.String(pkgFlag.Name) == "" { utils.Fatalf("No destination package specified (--pkg)") @@ -156,9 +180,32 @@ func abigen(c *cli.Context) error { utils.Fatalf("Failed to parse excludes: %v", err) } } + var err error var contracts map[string]*compiler.Contract - if c.IsSet(jsonFlag.Name) { + switch { + case c.IsSet(solFlag.Name): + contracts, err = compiler.CompileSolidity(c.String(solcFlag.Name), c.String(solFlag.Name)) + if err != nil { + utils.Fatalf("Failed to build Solidity contract: %v", err) + } + case c.IsSet(vyFlag.Name): + output, err := compiler.CompileVyper(c.String(vyperFlag.Name), c.String(vyFlag.Name)) + if err != nil { + utils.Fatalf("Failed to build Vyper contract: %v", err) + } + contracts = make(map[string]*compiler.Contract) + for n, contract := range output { + name := n + // Sanitize the combined json names to match the + // format expected by solidity. + if !strings.Contains(n, ":") { + // Remove extra path components + name = abi.ToCamelCase(strings.TrimSuffix(filepath.Base(name), ".vy")) + } + contracts[name] = contract + } + case c.IsSet(jsonFlag.Name): var ( input = c.String(jsonFlag.Name) jsonOutput []byte @@ -177,6 +224,7 @@ func abigen(c *cli.Context) error { utils.Fatalf("Failed to read contract information from json output: %v", err) } } + // Gather all non-excluded contract for binding for name, contract := range contracts { // fully qualified name is of the form : diff --git a/common/compiler/helpers.go b/common/compiler/helpers.go index 063fc10811025..59d242af3df6d 100644 --- a/common/compiler/helpers.go +++ b/common/compiler/helpers.go @@ -17,6 +17,14 @@ // Package compiler wraps the Solidity and Vyper compiler executables (solc; vyper). package compiler +import ( + "bytes" + "os" + "regexp" +) + +var versionRegexp = regexp.MustCompile(`([0-9]+)\.([0-9]+)\.([0-9]+)`) + // Contract contains information about a compiled contract, alongside its code and runtime code. type Contract struct { Code string `json:"code"` @@ -43,3 +51,15 @@ type ContractInfo struct { DeveloperDoc interface{} `json:"developerDoc"` Metadata string `json:"metadata"` } + +func slurpFiles(files []string) (string, error) { + var concat bytes.Buffer + for _, file := range files { + content, err := os.ReadFile(file) + if err != nil { + return "", err + } + concat.Write(content) + } + return concat.String(), nil +} diff --git a/common/compiler/solidity.go b/common/compiler/solidity.go index 9de94017c2edf..4be48ec0ebb1e 100644 --- a/common/compiler/solidity.go +++ b/common/compiler/solidity.go @@ -14,14 +14,26 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -// Package compiler wraps the ABI compilation outputs. +// Package compiler wraps the Solidity and Vyper compiler executables (solc; vyper). package compiler import ( + "bytes" "encoding/json" + "errors" "fmt" + "os/exec" + "strconv" + "strings" ) +// Solidity contains information about the solidity compiler. +type Solidity struct { + Path, Version, FullVersion string + Major, Minor, Patch int + ExtraAllowedPath []string +} + // --combined-output format type solcOutput struct { Contracts map[string]struct { @@ -47,6 +59,109 @@ type solcOutputV8 struct { Version string } +func (s *Solidity) allowedPaths() string { + paths := []string{".", "./", "../"} // default to support relative paths + if len(s.ExtraAllowedPath) > 0 { + paths = append(paths, s.ExtraAllowedPath...) + } + return strings.Join(paths, ", ") +} + +func (s *Solidity) makeArgs() []string { + p := []string{ + "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,abi,userdoc,devdoc", + "--optimize", // code optimizer switched on + "--allow-paths", s.allowedPaths(), + } + if s.Major > 0 || s.Minor > 4 || s.Patch > 6 { + p[1] += ",metadata,hashes" + } + return p +} + +// SolidityVersion runs solc and parses its version output. +func SolidityVersion(solc string) (*Solidity, error) { + if solc == "" { + solc = "solc" + } + var out bytes.Buffer + cmd := exec.Command(solc, "--version") + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, err + } + matches := versionRegexp.FindStringSubmatch(out.String()) + if len(matches) != 4 { + return nil, fmt.Errorf("can't parse solc version %q", out.String()) + } + s := &Solidity{Path: cmd.Path, FullVersion: out.String(), Version: matches[0]} + if s.Major, err = strconv.Atoi(matches[1]); err != nil { + return nil, err + } + if s.Minor, err = strconv.Atoi(matches[2]); err != nil { + return nil, err + } + if s.Patch, err = strconv.Atoi(matches[3]); err != nil { + return nil, err + } + return s, nil +} + +// CompileSolidityString builds and returns all the contracts contained within a source string. +func CompileSolidityString(solc, source string) (map[string]*Contract, error) { + if len(source) == 0 { + return nil, errors.New("solc: empty source string") + } + s, err := SolidityVersion(solc) + if err != nil { + return nil, err + } + return s.CompileSource(source) +} + +// CompileSolidity compiles all given Solidity source files. +func CompileSolidity(solc string, sourcefiles ...string) (map[string]*Contract, error) { + if len(sourcefiles) == 0 { + return nil, errors.New("solc: no source files") + } + s, err := SolidityVersion(solc) + if err != nil { + return nil, err + } + + return s.CompileFiles(sourcefiles...) +} + +// CompileSource builds and returns all the contracts contained within a source string. +func (s *Solidity) CompileSource(source string) (map[string]*Contract, error) { + args := append(s.makeArgs(), "--") + cmd := exec.Command(s.Path, append(args, "-")...) + cmd.Stdin = strings.NewReader(source) + return s.run(cmd, source) +} + +// CompileFiles compiles all given Solidity source files. +func (s *Solidity) CompileFiles(sourcefiles ...string) (map[string]*Contract, error) { + source, err := slurpFiles(sourcefiles) + if err != nil { + return nil, err + } + args := append(s.makeArgs(), "--") + cmd := exec.Command(s.Path, append(args, sourcefiles...)...) + return s.run(cmd, source) +} + +func (s *Solidity) run(cmd *exec.Cmd, source string) (map[string]*Contract, error) { + var stderr, stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("solc: %v\n%s", err, stderr.Bytes()) + } + return ParseCombinedJSON(stdout.Bytes(), source, s.Version, s.Version, strings.Join(s.makeArgs(), " ")) +} + // ParseCombinedJSON takes the direct output of a solc --combined-output run and // parses it into a map of string contract name to Contract structs. The // provided source, language and compiler version, and compiler options are all diff --git a/common/compiler/solidity_test.go b/common/compiler/solidity_test.go new file mode 100644 index 0000000000000..8d02eb4e7852b --- /dev/null +++ b/common/compiler/solidity_test.go @@ -0,0 +1,78 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package compiler + +import ( + "os/exec" + "testing" +) + +const ( + testSource = ` + pragma solidity >0.0.0; + contract test { + /// @notice Will multiply ` + "`a`" + ` by 7. + function multiply(uint a) public returns(uint d) { + return a * 7; + } + } + ` +) + +func skipWithoutSolc(t *testing.T) { + if _, err := exec.LookPath("solc"); err != nil { + t.Skip(err) + } +} + +func TestSolidityCompiler(t *testing.T) { + skipWithoutSolc(t) + + contracts, err := CompileSolidityString("", testSource) + if err != nil { + t.Fatalf("error compiling source. result %v: %v", contracts, err) + } + if len(contracts) != 1 { + t.Errorf("one contract expected, got %d", len(contracts)) + } + c, ok := contracts["test"] + if !ok { + c, ok = contracts[":test"] + if !ok { + t.Fatal("info for contract 'test' not present in result") + } + } + if c.Code == "" { + t.Error("empty code") + } + if c.Info.Source != testSource { + t.Error("wrong source") + } + if c.Info.CompilerVersion == "" { + t.Error("empty version") + } +} + +func TestSolidityCompileError(t *testing.T) { + skipWithoutSolc(t) + + contracts, err := CompileSolidityString("", testSource[4:]) + if err == nil { + t.Errorf("error expected compiling source. got none. result %v", contracts) + } + t.Logf("error: %v", err) +} diff --git a/common/compiler/test.v.py b/common/compiler/test.v.py new file mode 100644 index 0000000000000..f7433fd80e794 --- /dev/null +++ b/common/compiler/test.v.py @@ -0,0 +1,3 @@ +@public + def test(): + hello: int128 diff --git a/common/compiler/test_bad.v.py b/common/compiler/test_bad.v.py new file mode 100644 index 0000000000000..d9895bee9ca52 --- /dev/null +++ b/common/compiler/test_bad.v.py @@ -0,0 +1,3 @@ +lic + def test(): + hello: int128 diff --git a/common/compiler/vyper.go b/common/compiler/vyper.go new file mode 100644 index 0000000000000..f3b9397e6913e --- /dev/null +++ b/common/compiler/vyper.go @@ -0,0 +1,146 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package compiler wraps the Solidity and Vyper compiler executables (solc; vyper). +package compiler + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "os/exec" + "strconv" + "strings" +) + +// Vyper contains information about the vyper compiler. +type Vyper struct { + Path, Version, FullVersion string + Major, Minor, Patch int +} + +func (s *Vyper) makeArgs() []string { + p := []string{ + "-f", "combined_json", + } + return p +} + +// VyperVersion runs vyper and parses its version output. +func VyperVersion(vyper string) (*Vyper, error) { + if vyper == "" { + vyper = "vyper" + } + var out bytes.Buffer + cmd := exec.Command(vyper, "--version") + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + return nil, err + } + matches := versionRegexp.FindStringSubmatch(out.String()) + if len(matches) != 4 { + return nil, fmt.Errorf("can't parse vyper version %q", out.String()) + } + s := &Vyper{Path: cmd.Path, FullVersion: out.String(), Version: matches[0]} + if s.Major, err = strconv.Atoi(matches[1]); err != nil { + return nil, err + } + if s.Minor, err = strconv.Atoi(matches[2]); err != nil { + return nil, err + } + if s.Patch, err = strconv.Atoi(matches[3]); err != nil { + return nil, err + } + return s, nil +} + +// CompileVyper compiles all given Vyper source files. +func CompileVyper(vyper string, sourcefiles ...string) (map[string]*Contract, error) { + if len(sourcefiles) == 0 { + return nil, errors.New("vyper: no source files") + } + source, err := slurpFiles(sourcefiles) + if err != nil { + return nil, err + } + s, err := VyperVersion(vyper) + if err != nil { + return nil, err + } + args := s.makeArgs() + cmd := exec.Command(s.Path, append(args, sourcefiles...)...) + return s.run(cmd, source) +} + +func (s *Vyper) run(cmd *exec.Cmd, source string) (map[string]*Contract, error) { + var stderr, stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + if err := cmd.Run(); err != nil { + return nil, fmt.Errorf("vyper: %v\n%s", err, stderr.Bytes()) + } + + return ParseVyperJSON(stdout.Bytes(), source, s.Version, s.Version, + strings.Join(s.makeArgs(), " ")) +} + +// ParseVyperJSON takes the direct output of a vyper --f combined_json run and +// parses it into a map of string contract name to Contract structs. The +// provided source, language and compiler version, and compiler options are all +// passed through into the Contract structs. +// +// The vyper output is expected to contain ABI and source mapping. +// +// Returns an error if the JSON is malformed or missing data, or if the JSON +// embedded within the JSON is malformed. +func ParseVyperJSON(combinedJSON []byte, source string, languageVersion string, + compilerVersion string, compilerOptions string) (map[string]*Contract, error) { + var output map[string]interface{} + if err := json.Unmarshal(combinedJSON, &output); err != nil { + return nil, err + } + + // Compilation succeeded, assemble and return the contracts. + contracts := make(map[string]*Contract) + for name, info := range output { + // Parse the individual compilation results. + if name == "version" { + continue + } + c := info.(map[string]interface{}) + + contracts[name] = &Contract{ + Code: c["bytecode"].(string), + RuntimeCode: c["bytecode_runtime"].(string), + Info: ContractInfo{ + Source: source, + Language: "Vyper", + LanguageVersion: languageVersion, + CompilerVersion: compilerVersion, + CompilerOptions: compilerOptions, + SrcMap: c["source_map"], + SrcMapRuntime: "", + AbiDefinition: c["abi"], + UserDoc: "", + DeveloperDoc: "", + Metadata: "", + }, + } + } + return contracts, nil +} diff --git a/common/compiler/vyper_test.go b/common/compiler/vyper_test.go new file mode 100644 index 0000000000000..21945cbf4646f --- /dev/null +++ b/common/compiler/vyper_test.go @@ -0,0 +1,72 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package compiler + +import ( + "os/exec" + "testing" +) + +func skipWithoutVyper(t *testing.T) { + if _, err := exec.LookPath("vyper"); err != nil { + t.Skip(err) + } +} + +func TestVyperCompiler(t *testing.T) { + skipWithoutVyper(t) + + testSource := []string{"test.v.py"} + source, err := slurpFiles(testSource) + if err != nil { + t.Error("couldn't read test files") + } + contracts, err := CompileVyper("", testSource...) + if err != nil { + t.Fatalf("error compiling test.v.py. result %v: %v", contracts, err) + } + if len(contracts) != 1 { + t.Errorf("one contract expected, got %d", len(contracts)) + } + c, ok := contracts["test.v.py"] + if !ok { + c, ok = contracts[":test"] + if !ok { + t.Fatal("info for contract 'test.v.py' not present in result") + } + } + if c.Code == "" { + t.Error("empty code") + } + if c.Info.Source != source { + t.Error("wrong source") + } + if c.Info.CompilerVersion == "" { + t.Error("empty version") + } +} + +func TestVyperCompileError(t *testing.T) { + skipWithoutVyper(t) + + contracts, err := CompileVyper("", "test_bad.v.py") + if err == nil { + t.Errorf("error expected compiling test_bad.v.py. got none. result %v", + contracts) + } + t.Logf("error: %v", err) +}