1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
package bindgen
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path"
"strings"
"github.com/ethereum/go-ethereum/log"
)
type BindGenGeneratorBase struct {
MetadataOut string
BindingsPackageName string
MonorepoBasePath string
ContractsListPath string
Logger log.Logger
}
type contractsList struct {
Local []string `json:"local"`
Remote []RemoteContract `json:"remote"`
}
// readContractList reads a JSON file from the given `filePath` and unmarshals
// its content into the provided result interface. It logs the path of the file
// being read.
//
// Parameters:
// - logger: An instance of go-ethereum/log
// - filePath: The path to the JSON file to be read.
// - result: A pointer to the structure where the JSON data will be unmarshaled.
//
// Returns:
// - An error if reading the file or unmarshaling fails, nil otherwise.
func readContractList(logger log.Logger, filePath string) (contractsList, error) {
logger.Debug("Reading contract list", "filePath", filePath)
var contracts contractsList
contractData, err := os.ReadFile(filePath)
if err != nil {
return contracts, err
}
return contracts, json.Unmarshal(contractData, &contracts)
}
// mkTempArtifactsDir creates a temporary directory with a "op-bindings" prefix
// for holding contract artifacts. The path to the created directory is logged.
//
// Parameters:
// - logger: An instance of go-ethereum/log
//
// Returns:
// - The path to the created temporary directory.
// - An error if the directory creation fails, nil otherwise.
func mkTempArtifactsDir(logger log.Logger) (string, error) {
dir, err := os.MkdirTemp("", "op-bindings")
if err != nil {
return "", err
}
logger.Debug("Created temporary artifacts directory", "dir", dir)
return dir, nil
}
// writeContractArtifacts writes the provided ABI and bytecode data to respective
// files in the specified temporary directory. The naming convention for these
// files is based on the provided contract name. The ABI data is written to a file
// with a ".abi" extension, and the bytecode data is written to a file with a ".bin"
// extension.
//
// Parameters:
// - logger: An instance of go-ethereum/log
// - tempDirPath: The directory path where the ABI and bytecode files will be written.
// - contractName: The name of the contract, used to create the filenames.
// - abi: The ABI data of the contract.
// - bytecode: The bytecode of the contract.
//
// Returns:
// - The full path to the written ABI file.
// - The full path to the written bytecode file.
// - An error if writing either file fails, nil otherwise.
func writeContractArtifacts(logger log.Logger, tempDirPath, contractName string, abi, bytecode []byte) (string, string, error) {
logger.Debug("Writing ABI and bytecode to temporary artifacts directory", "contractName", contractName, "tempDirPath", tempDirPath)
abiFilePath := path.Join(tempDirPath, contractName+".abi")
if err := os.WriteFile(abiFilePath, abi, 0o600); err != nil {
return "", "", fmt.Errorf("error writing %s's ABI file: %w", contractName, err)
}
bytecodeFilePath := path.Join(tempDirPath, contractName+".bin")
if err := os.WriteFile(bytecodeFilePath, bytecode, 0o600); err != nil {
return "", "", fmt.Errorf("error writing %s's bytecode file: %w", contractName, err)
}
return abiFilePath, bytecodeFilePath, nil
}
// genContractBindings generates Go bindings for an Ethereum contract using
// the provided ABI and bytecode files. The bindings are generated using the
// `abigen` tool and are written to the specified Go package directory. The
// generated file's name is based on the provided contract name and will have
// a ".go" extension. The generated bindings will be part of the provided Go
// package.
//
// Parameters:
// - logger: An instance of go-ethereum/log
// - abiFilePath: The path to the ABI file for the contract.
// - bytecodeFilePath: The path to the bytecode file for the contract.
// - goPackageName: The name of the Go package where the bindings will be written.
// - contractName: The name of the contract, used for naming the output file and
// defining the type in the generated bindings.
//
// Returns:
// - An error if there's an issue during any step of the binding generation process,
// nil otherwise.
//
// Note: This function relies on the external `abigen` tool, which should be
// installed and available in the system's PATH.
func genContractBindings(logger log.Logger, monorepoRootPath, abiFilePath, bytecodeFilePath, goPackageName, contractName string) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("error getting cwd: %w", err)
}
outFilePath := path.Join(cwd, goPackageName, strings.ToLower(contractName)+".go")
var existingOutput []byte
if _, err := os.Stat(outFilePath); err == nil {
existingOutput, err = os.ReadFile(outFilePath)
if err != nil {
return fmt.Errorf("error reading existing bindings output file, outFilePath: %s err: %w", outFilePath, err)
}
}
if monorepoRootPath != "" {
logger.Debug("Checking abigen version")
// Fetch installed abigen version (format: abigen version X.Y.Z-<stable/nightly>-<commit_sha>)
cmd := exec.Command("abigen", "--version")
var versionBuf bytes.Buffer
cmd.Stdout = bufio.NewWriter(&versionBuf)
if err := cmd.Run(); err != nil {
return fmt.Errorf("error fetching abigen version: %w", err)
}
abigenVersion := bytes.Trim(versionBuf.Bytes(), "\n")
// Fetch expected abigen version (format: vX.Y.Z)
expectedAbigenVersion, err := readExpectedAbigenVersion(monorepoRootPath)
if err != nil {
return fmt.Errorf("error fetching the expected abigen version: %w", err)
}
if !bytes.Contains(abigenVersion, []byte(expectedAbigenVersion)) {
return fmt.Errorf("abigen version mismatch, expected %s, got %s. Please run `pnpm install:abigen` in the monorepo root", expectedAbigenVersion, abigenVersion)
}
} else {
logger.Debug("No monorepo root path provided, skipping abigen version check")
}
logger.Debug("Generating contract bindings", "contractName", contractName, "outFilePath", outFilePath)
cmd := exec.Command("abigen", "--abi", abiFilePath, "--bin", bytecodeFilePath, "--pkg", goPackageName, "--type", contractName, "--out", outFilePath)
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("error running abigen for %s: %w", contractName, err)
}
if len(existingOutput) != 0 {
newOutput, err := os.ReadFile(outFilePath)
if err != nil {
return fmt.Errorf("error reading new file: %w", err)
}
if bytes.Equal(existingOutput, newOutput) {
logger.Debug("No changes detected in the contract bindings", "contractName", contractName)
} else {
logger.Warn("Changes detected in the contract bindings, old bindings have been overwritten", "contractName", contractName)
}
} else {
logger.Debug("No existing contract bindings found, skipping comparison", "contractName", contractName)
}
return nil
}
// Versions is a struct for holding the versions of the tools used in the monorepo
type Versions struct {
Abigen string `json:"abigen"`
Foundry string `json:"foundry"`
Geth string `json:"geth"`
Nvm string `json:"nvm"`
Slither string `json:"slither"`
Kontrol string `json:"kontrol"`
}
// readExpectedAbigenVersion reads the expected abigen version from the monorepo's
// versions.json file. This function will remove the 'v' prefix from the version
// string.
//
// Parameters:
// - monorepoRootPath: The path to the monorepo's root directory.
//
// Returns:
// - The expected abigen version.
// - An error if the versions.json file cannot be read or parsed, nil otherwise.
func readExpectedAbigenVersion(monorepoRootPath string) (string, error) {
// Open the version control file
jsonFile, err := os.Open(path.Join(monorepoRootPath, "versions.json"))
if err != nil {
return "", fmt.Errorf("error reading versions.json file: %w", err)
}
defer jsonFile.Close()
// Parse the version control file
byteValue, _ := io.ReadAll(jsonFile)
var versions Versions
if err := json.Unmarshal(byteValue, &versions); err != nil {
return "", fmt.Errorf("error parsing versions.json file: %w", err)
}
// Trim the 'v' prefix from the version string
return strings.Trim(versions.Abigen, "v"), nil
}