diff --git a/cmd/abigen/main.go b/cmd/abigen/main.go index 9a19efe96f..46a9f98719 100644 --- a/cmd/abigen/main.go +++ b/cmd/abigen/main.go @@ -19,6 +19,7 @@ package main import ( "encoding/json" "fmt" + "io" "os" "strings" @@ -59,6 +60,15 @@ var ( 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", @@ -87,6 +97,8 @@ func init() { typeFlag, solFlag, solcFlag, + vyFlag, + vyperFlag, excFlag, pkgFlag, outFlag, @@ -96,11 +108,14 @@ func init() { } func abigen(c *cli.Context) error { - if c.String(abiFlag.Name) == "" && c.String(solFlag.Name) == "" { - fmt.Printf("No contract ABI (--abi) or Solidity source (--sol) specified\n") + if c.String(abiFlag.Name) == "" && c.String(solFlag.Name) == "" && c.String(vyFlag.Name) == "" { + fmt.Printf("No contract ABI (--abi), Solidity source (--sol), or Vyper source (--vy) specified\n") os.Exit(-1) - } else if (c.String(abiFlag.Name) != "" || c.String(binFlag.Name) != "" || c.String(typeFlag.Name) != "") && c.String(solFlag.Name) != "" { - fmt.Printf("Contract ABI (--abi), bytecode (--bin) and type (--type) flags are mutually exclusive with the Solidity source (--sol) flag\n") + } else if (c.String(abiFlag.Name) != "" || c.String(binFlag.Name) != "" || c.String(typeFlag.Name) != "") && (c.String(solFlag.Name) != "" || c.String(vyFlag.Name) != "") { + fmt.Printf("Contract ABI (--abi), bytecode (--bin) and type (--type) flags are mutually exclusive with the Solidity (--sol) and Vyper (--vy) flags\n") + os.Exit(-1) + } else if c.String(solFlag.Name) != "" && c.String(vyFlag.Name) == "" { + fmt.Printf("Solidity (--sol) and Vyper (--vy) flags are mutually exclusive\n") os.Exit(-1) } if c.String(pkgFlag.Name) == "" { @@ -121,23 +136,47 @@ func abigen(c *cli.Context) error { bins []string types []string ) - if c.String(solFlag.Name) != "" { + if c.String(solFlag.Name) != "" || c.String(vyFlag.Name) != "" || (c.String(abiFlag.Name) == "-" && c.String(pkgFlag.Name) == "") { // Generate the list of types to exclude from binding exclude := make(map[string]bool) for _, kind := range strings.Split(c.String(excFlag.Name), ",") { exclude[strings.ToLower(kind)] = true } - contracts, err := compiler.CompileSolidity(c.String(solcFlag.Name), c.String(solFlag.Name)) - if err != nil { - fmt.Printf("Failed to build Solidity contract: %v\n", err) - os.Exit(-1) + + var contracts map[string]*compiler.Contract + var err error + + switch { + case c.String(solFlag.Name) != "": + contracts, err = compiler.CompileSolidity(c.String(solcFlag.Name), c.String(solFlag.Name)) + if err != nil { + fmt.Printf("Failed to build Solidity contract: %v\n", err) + os.Exit(-1) + } + case c.String(vyFlag.Name) != "": + contracts, err = compiler.CompileVyper(c.String(vyperFlag.Name), c.String(vyFlag.Name)) + if err != nil { + fmt.Printf("Failed to build Vyper contract: %v\n", err) + os.Exit(-1) + } + default: + contracts, err = contractsFromStdin() + if err != nil { + fmt.Printf("Failed to read input ABIs from STDIN: %v\n", err) + os.Exit(-1) + } } + // Gather all non-excluded contract for binding for name, contract := range contracts { if exclude[strings.ToLower(name)] { continue } - abi, _ := json.Marshal(contract.Info.AbiDefinition) // Flatten the compiler parse + abi, err := json.Marshal(contract.Info.AbiDefinition) // Flatten the compiler parse + if err != nil { + fmt.Printf("Failed to parse ABIs from compiler output: %v\n", err) + os.Exit(-1) + } abis = append(abis, string(abi)) bins = append(bins, contract.Code) @@ -146,14 +185,20 @@ func abigen(c *cli.Context) error { } } else { // Otherwise load up the ABI, optional bytecode and type name from the parameters - abi, err := os.ReadFile(c.String(abiFlag.Name)) + var abi []byte + var err error + if c.String(abiFlag.Name) == "-" { + abi, err = io.ReadAll(os.Stdin) + } else { + abi, err = os.ReadFile(c.String(abiFlag.Name)) + } if err != nil { fmt.Printf("Failed to read input ABI: %v\n", err) os.Exit(-1) } abis = append(abis, string(abi)) - bin := []byte{} + var bin []byte if c.String(binFlag.Name) != "" { if bin, err = os.ReadFile(c.String(binFlag.Name)); err != nil { fmt.Printf("Failed to read input bytecode: %v\n", err) @@ -194,3 +239,11 @@ func main() { os.Exit(1) } } + +func contractsFromStdin() (map[string]*compiler.Contract, error) { + bytes, err := io.ReadAll(os.Stdin) + if err != nil { + return nil, err + } + return compiler.ParseCombinedJSON(bytes, "", "", "", "") +} diff --git a/common/compiler/helpers.go b/common/compiler/helpers.go new file mode 100644 index 0000000000..c2d4a20e36 --- /dev/null +++ b/common/compiler/helpers.go @@ -0,0 +1,64 @@ +// 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" + "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"` + RuntimeCode string `json:"runtime-code"` + Info ContractInfo `json:"info"` +} + +// ContractInfo contains information about a compiled contract, including access +// to the ABI definition, source mapping, user and developer docs, and metadata. +// +// Depending on the source, language version, compiler version, and compiler +// options will provide information about how the contract was compiled. +type ContractInfo struct { + Source string `json:"source"` + Language string `json:"language"` + LanguageVersion string `json:"languageVersion"` + CompilerVersion string `json:"compilerVersion"` + CompilerOptions string `json:"compilerOptions"` + SrcMap interface{} `json:"srcMap"` + SrcMapRuntime string `json:"srcMapRuntime"` + AbiDefinition interface{} `json:"abiDefinition"` + UserDoc interface{} `json:"userDoc"` + 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 f340201239..7ed9c2633e 100644 --- a/common/compiler/solidity.go +++ b/common/compiler/solidity.go @@ -1,4 +1,4 @@ -// Copyright 2015 The go-ethereum Authors +// 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 @@ -14,7 +14,7 @@ // 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 compiler executable (solc). +// Package compiler wraps the Solidity and Vyper compiler executables (solc; vyper). package compiler import ( @@ -22,32 +22,11 @@ import ( "encoding/json" "errors" "fmt" - "os" "os/exec" - "regexp" "strconv" "strings" ) -var versionRegexp = regexp.MustCompile(`([0-9]+)\.([0-9]+)\.([0-9]+)`) - -type Contract struct { - Code string `json:"code"` - Info ContractInfo `json:"info"` -} - -type ContractInfo struct { - Source string `json:"source"` - Language string `json:"language"` - LanguageVersion string `json:"languageVersion"` - CompilerVersion string `json:"compilerVersion"` - CompilerOptions string `json:"compilerOptions"` - AbiDefinition interface{} `json:"abiDefinition"` - UserDoc interface{} `json:"userDoc"` - DeveloperDoc interface{} `json:"developerDoc"` - Metadata string `json:"metadata"` -} - // Solidity contains information about the solidity compiler. type Solidity struct { Path, Version, FullVersion string @@ -57,14 +36,16 @@ type Solidity struct { // --combined-output format type solcOutput struct { Contracts map[string]struct { - Bin, Abi, Devdoc, Userdoc, Metadata string + BinRuntime string `json:"bin-runtime"` + SrcMapRuntime string `json:"srcmap-runtime"` + Bin, SrcMap, Abi, Devdoc, Userdoc, Metadata string } Version string } func (s *Solidity) makeArgs() []string { p := []string{ - "--combined-json", "bin,abi,userdoc,devdoc", + "--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,abi,userdoc,devdoc", "--optimize", // code optimizer switched on } if s.Major > 0 || s.Minor > 4 || s.Patch > 6 { @@ -142,8 +123,22 @@ func (s *Solidity) run(cmd *exec.Cmd, source string) (map[string]*Contract, erro 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 +// passed through into the Contract structs. +// +// The solc output is expected to contain ABI, source mapping, user docs, and dev docs. +// +// Returns an error if the JSON is malformed or missing data, or if the JSON +// embedded within the JSON is malformed. +func ParseCombinedJSON(combinedJSON []byte, source string, languageVersion string, compilerVersion string, compilerOptions string) (map[string]*Contract, error) { var output solcOutput - if err := json.Unmarshal(stdout.Bytes(), &output); err != nil { + if err := json.Unmarshal(combinedJSON, &output); err != nil { return nil, err } @@ -164,13 +159,16 @@ func (s *Solidity) run(cmd *exec.Cmd, source string) (map[string]*Contract, erro return nil, fmt.Errorf("solc: error reading dev doc: %v", err) } contracts[name] = &Contract{ - Code: "0x" + info.Bin, + Code: "0x" + info.Bin, + RuntimeCode: "0x" + info.BinRuntime, Info: ContractInfo{ Source: source, Language: "Solidity", - LanguageVersion: s.Version, - CompilerVersion: s.Version, - CompilerOptions: strings.Join(s.makeArgs(), " "), + LanguageVersion: languageVersion, + CompilerVersion: compilerVersion, + CompilerOptions: compilerOptions, + SrcMap: info.SrcMap, + SrcMapRuntime: info.SrcMapRuntime, AbiDefinition: abi, UserDoc: userdoc, DeveloperDoc: devdoc, @@ -180,15 +178,3 @@ func (s *Solidity) run(cmd *exec.Cmd, source string) (map[string]*Contract, erro } return contracts, nil } - -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_test.go b/common/compiler/solidity_test.go index 0f6f0f7ce7..bd68eaee31 100644 --- a/common/compiler/solidity_test.go +++ b/common/compiler/solidity_test.go @@ -1,4 +1,4 @@ -// Copyright 2015 The go-ethereum Authors +// 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 @@ -39,7 +39,7 @@ func skipWithoutSolc(t *testing.T) { } } -func TestCompiler(t *testing.T) { +func TestSolidityCompiler(t *testing.T) { t.SkipNow() skipWithoutSolc(t) @@ -68,7 +68,7 @@ func TestCompiler(t *testing.T) { } } -func TestCompileError(t *testing.T) { +func TestSolidityCompileError(t *testing.T) { skipWithoutSolc(t) contracts, err := CompileSolidityString("", testSource[4:]) diff --git a/common/compiler/test.v.py b/common/compiler/test.v.py new file mode 100644 index 0000000000..35af56c8f6 --- /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 0000000000..443ef78263 --- /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 0000000000..a9bca95e59 --- /dev/null +++ b/common/compiler/vyper.go @@ -0,0 +1,144 @@ +// 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 0000000000..7761c92aff --- /dev/null +++ b/common/compiler/vyper_test.go @@ -0,0 +1,71 @@ +// 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) +}