11package main
22
33import (
4+ "bytes"
5+ "encoding/json"
46 "errors"
57 "fmt"
8+ "go/format"
9+ "go/parser"
10+ "go/token"
611 "io"
712 "log"
813 "os"
914 "os/exec"
1015 "path/filepath"
16+ "regexp"
1117 "strings"
18+ "text/template"
1219
20+ "github.com/ethereum/go-ethereum/common/compiler"
1321 "github.com/spf13/cobra"
22+ "golang.org/x/tools/go/ast/astutil"
23+
24+ _ "embed"
1425)
1526
27+ const srcMapFlag = "experimental_src_map"
28+
1629func init () {
17- rootCmd . AddCommand ( & cobra.Command {
30+ cmd := & cobra.Command {
1831 Use : "gen" ,
1932 Short : "Compiles Solidity contracts to generate Go ABI bindings with go:generate" ,
2033 RunE : gen ,
@@ -29,12 +42,16 @@ func init() {
2942 }
3043 return nil
3144 },
32- })
45+ }
46+
47+ cmd .Flags ().Bool (srcMapFlag , false , "Generate source maps to determine Solidity code location from EVM traces" )
48+
49+ rootCmd .AddCommand (cmd )
3350}
3451
3552// gen runs `solc | abigen` on the Solidity source files passed as the args.
3653// TODO: support wildcard / glob matching of files.
37- func gen (_ * cobra.Command , args []string ) (retErr error ) {
54+ func gen (cmd * cobra.Command , args []string ) (retErr error ) {
3855 pwd , err := os .Getwd ()
3956 if err != nil {
4057 return fmt .Errorf ("os.Getwd(): %v" , err )
@@ -62,7 +79,7 @@ func gen(_ *cobra.Command, args []string) (retErr error) {
6279 args ,
6380 "--base-path" , basePath ,
6481 "--include-path" , filepath .Join (basePath , "node_modules" ),
65- "--combined-json" , "abi,bin" ,
82+ "--combined-json" , "abi,bin,bin-runtime,hashes,srcmap-runtime " ,
6683 )
6784 solc := exec .Command ("solc" , args ... )
6885 solc .Stderr = os .Stderr
@@ -75,13 +92,16 @@ func gen(_ *cobra.Command, args []string) (retErr error) {
7592 "abigen" ,
7693 "--combined-json" , "/dev/stdin" ,
7794 "--pkg" , pkg ,
78- "--out" , "generated.go" ,
7995 )
8096 abigen .Stderr = os .Stderr
8197
8298 r , w := io .Pipe ()
8399 solc .Stdout = w
84- abigen .Stdin = r
100+ combinedJSON := bytes .NewBuffer (nil )
101+ abigen .Stdin = io .TeeReader (r , combinedJSON )
102+
103+ generated := bytes .NewBuffer (nil )
104+ abigen .Stdout = generated
85105
86106 if err := solc .Start (); err != nil {
87107 return fmt .Errorf ("start `solc`: %v" , err )
@@ -99,5 +119,146 @@ func gen(_ *cobra.Command, args []string) (retErr error) {
99119 if err := abigen .Wait (); err != nil {
100120 return fmt .Errorf ("`abigen` returned: %v" , err )
101121 }
102- return r .Close ()
122+ if err := r .Close (); err != nil {
123+ return fmt .Errorf ("close read-half of pipe from solc to abigen: %v" , err )
124+ }
125+
126+ extend , err := cmd .Flags ().GetBool (srcMapFlag )
127+ if err != nil {
128+ return fmt .Errorf ("%T.Flags().GetBool(%q): %v" , cmd , srcMapFlag , err )
129+ }
130+ if ! extend {
131+ return os .WriteFile ("generated.go" , generated .Bytes (), 0644 )
132+ }
133+
134+ out , err := extendGeneratedCode (generated , combinedJSON )
135+ if err != nil {
136+ return err
137+ }
138+ return os .WriteFile ("generated.go" , out , 0644 )
139+ }
140+
141+ var (
142+ //go:embed gen_extra.go.tmpl
143+ extraCode string
144+
145+ // extraTemplate is the template for use by extendGeneratedCode().
146+ extraTemplate = template .Must (
147+ template .New ("extra" ).
148+ Funcs (template.FuncMap {
149+ "quote" : func (s interface {}) string {
150+ return fmt .Sprintf ("%q" , s )
151+ },
152+ "stringSlice" : func (strs []string ) string {
153+ q := make ([]string , len (strs ))
154+ for i , s := range strs {
155+ q [i ] = fmt .Sprintf ("%q" , s )
156+ }
157+ return fmt .Sprintf ("[]string{%s}" , strings .Join (q , ", " ))
158+ },
159+ "contract" : func (s string ) (string , error ) {
160+ parts := strings .Split (s , ".sol:" )
161+ if len (parts ) != 2 {
162+ return "" , fmt .Errorf ("invalid contract name %q must have format path/to/file.sol:ContractName" , s )
163+ }
164+ return parts [1 ], nil
165+ },
166+ }).
167+ Parse (extraCode ),
168+ )
169+
170+ // Regular expressions for modifying abigen-generated code to work with the
171+ // extraTemplate code above.
172+ deployedRegexp = regexp .MustCompile (`^\s*return address, tx, &(.+?)\{.*Transactor.*\}, nil\s*$` )
173+ // Note the option for matching strings.Replace or strings.ReplaceAll due to
174+ // a recent change in abigen.
175+ libReplacementRegexp = regexp .MustCompile (`^\s*(.+?)Bin = strings.Replace(?:All)?\(.+?, "__\$([0-9a-f]{34})\$__", (.+?)(?:, -1)?\)\s*$` )
176+ // TODO(aschlosberg) replace regular expressions with a more explicit
177+ // approach for modifying the output code. This likely requires a PR to the
178+ // go-ethereum repo to allow bind.Bind (+/- abigen) to accept an alternate
179+ // template.
180+ )
181+
182+ // extendGeneratedCode adds ethier-specific functionality to code generated by
183+ // abigen, allowing for interoperability with the ethier/solidity package for
184+ // source-map interpretation at runtime.
185+ func extendGeneratedCode (generated , combinedJSON * bytes.Buffer ) ([]byte , error ) {
186+ meta := struct {
187+ SourceList []string `json:"sourceList"`
188+ Version string `json:"version"`
189+
190+ Contracts map [string ]* compiler.Contract
191+ CombinedJSON string
192+ }{CombinedJSON : combinedJSON .String ()}
193+
194+ if err := json .Unmarshal (combinedJSON .Bytes (), & meta ); err != nil {
195+ return nil , fmt .Errorf ("json.Unmarshal([solc output], %T): %v" , & meta , err )
196+ }
197+
198+ cs , err := compiler .ParseCombinedJSON (combinedJSON .Bytes (), "" , "" , meta .Version , "" )
199+ if err != nil {
200+ return nil , fmt .Errorf ("compiler.ParseCombinedJSON(): %v" , err )
201+ }
202+ meta .Contracts = cs
203+ for k , c := range meta .Contracts {
204+ if c .RuntimeCode == "0x" {
205+ delete (meta .Contracts , k )
206+ }
207+ }
208+
209+ if err := extraTemplate .Execute (generated , meta ); err != nil {
210+ return nil , fmt .Errorf ("%T.Execute(): %v" , extraTemplate , err )
211+ }
212+
213+ // When using vm.Config.Trace, the only contract-identifying information is
214+ // the address to which the transaction was sent. We must therefore modify
215+ // every DeployFoo() function to save the address(es) at which the contract
216+ // is deployed.
217+ lines := strings .Split (generated .String (), "\n " )
218+ for i , l := range lines {
219+ matches := deployedRegexp .FindStringSubmatch (l )
220+ if len (matches ) == 0 {
221+ continue
222+ }
223+ lines [i ] = fmt .Sprintf (
224+ `deployedContracts[address] = %q // Added by ethier gen
225+ %s` ,
226+ matches [1 ], l ,
227+ )
228+ }
229+
230+ // Libraries have their addresses string-replaced directly into contract
231+ // code, which we need to mirror for the runtime code too.
232+ for i , l := range lines {
233+ matches := libReplacementRegexp .FindStringSubmatch (l )
234+ if len (matches ) == 0 {
235+ continue
236+ }
237+ lines [i ] = fmt .Sprintf (
238+ `%s
239+ RuntimeSourceMaps[%q].RuntimeCode = strings.Replace(RuntimeSourceMaps[%[2]q].RuntimeCode, "__$%s$__", %s, -1)` ,
240+ l , matches [1 ], matches [2 ], matches [3 ],
241+ )
242+ }
243+
244+ // Effectively the same as running goimports on the (ugly) generated code.
245+ fset := token .NewFileSet ()
246+ f , err := parser .ParseFile (fset , "generated.go" , strings .Join (lines , "\n " ), parser .ParseComments | parser .AllErrors )
247+ if err != nil {
248+ return nil , fmt .Errorf ("parser.ParseFile(%T, …): %v" , fset , err )
249+ }
250+ for _ , pkg := range []string {
251+ "github.com/ethereum/go-ethereum/common/compiler" ,
252+ "github.com/divergencetech/ethier/solidity" ,
253+ } {
254+ if ! astutil .AddImport (fset , f , pkg ) {
255+ return nil , fmt .Errorf ("add import %q to generated Go: %v" , pkg , err )
256+ }
257+ }
258+
259+ buf := bytes .NewBuffer (nil )
260+ if err := format .Node (buf , fset , f ); err != nil {
261+ return nil , fmt .Errorf ("format.Node(%T, %T, %T): %v" , buf , fset , f , err )
262+ }
263+ return buf .Bytes (), nil
103264}
0 commit comments