From e4536f9e7a06a308d9a3b6d79434460fbd30fe4d Mon Sep 17 00:00:00 2001 From: Wyatt Barnes Date: Thu, 16 Nov 2023 17:26:24 -1000 Subject: [PATCH] BindGen - local contracts refactor --- op-bindings/Makefile | 26 ++- op-bindings/artifacts.json | 98 +++++----- op-bindings/bindgen/generator_local.go | 253 +++++++++++++++++++++++++ op-bindings/bindgen/main.go | 150 +++++++++++++++ op-bindings/bindgen/utils.go | 131 +++++++++++++ op-bindings/gen/main.go | 241 ----------------------- op-e2e/config/init.go | 6 +- 7 files changed, 604 insertions(+), 301 deletions(-) create mode 100644 op-bindings/bindgen/generator_local.go create mode 100644 op-bindings/bindgen/main.go create mode 100644 op-bindings/bindgen/utils.go delete mode 100644 op-bindings/gen/main.go diff --git a/op-bindings/Makefile b/op-bindings/Makefile index 1a5b8360bfe18..906fc23de1e5a 100644 --- a/op-bindings/Makefile +++ b/op-bindings/Makefile @@ -3,6 +3,8 @@ SHELL := /usr/bin/env bash pkg := bindings monorepo-base := $(shell dirname $(realpath .)) contracts-dir := $(monorepo-base)/packages/contracts-bedrock +contracts-list := ./artifacts.json +log-level := info all: version mkdir bindings @@ -15,16 +17,22 @@ compile: forge clean && \ pnpm build -bindings: compile bindings-build +bindings: bindgen-local -bindings-build: - go run ./gen/main.go \ - -forge-artifacts $(contracts-dir)/forge-artifacts \ - -out ./bindings \ - -contracts ./artifacts.json \ - -source-maps MIPS,PreimageOracle \ - -package $(pkg) \ - -monorepo-base $(monorepo-base) +bindings-build: bindgen-generate-local + +bindgen-local: compile bindgen-generate-local + +bindgen-generate-local: + go run ./bindgen/ \ + generate \ + --metadata-out ./$(pkg) \ + --bindings-package $(pkg) \ + --contracts-list $(contracts-list) \ + --log.level $(log-level) \ + local \ + --source-maps-list MIPS,PreimageOracle \ + --forge-artifacts $(contracts-dir)/forge-artifacts mkdir: mkdir -p $(pkg) diff --git a/op-bindings/artifacts.json b/op-bindings/artifacts.json index af06e10a6373e..f216885aab145 100644 --- a/op-bindings/artifacts.json +++ b/op-bindings/artifacts.json @@ -1,48 +1,50 @@ -[ - "SystemConfig", - "L1CrossDomainMessenger", - "L1StandardBridge", - "OptimismPortal", - "L2OutputOracle", - "AddressManager", - "L1Block", - "L2ToL1MessagePasser", - "GasPriceOracle", - "L2CrossDomainMessenger", - "L2StandardBridge", - "L2ERC721Bridge", - "L1ERC721Bridge", - "OptimismMintableERC721Factory", - "SequencerFeeVault", - "BaseFeeVault", - "L1FeeVault", - "OptimismMintableERC20Factory", - "OptimismMintableERC20", - "LegacyERC20ETH", - "Proxy", - "ProxyAdmin", - "LegacyMessagePasser", - "ERC20", - "WETH9", - "DeployerWhitelist", - "L1BlockNumber", - "DisputeGameFactory", - "FaultDisputeGame", - "OutputBisectionGame", - "AlphabetVM", - "AlphabetVM2", - "StandardBridge", - "CrossDomainMessenger", - "MIPS", - "PreimageOracle", - "BlockOracle", - "EAS", - "SchemaRegistry", - "ProtocolVersions", - "Safe", - "SafeProxyFactory", - "DelayedVetoable", - "ISemver", - "StorageSetter", - "SuperchainConfig" -] +{ + "local": [ + "SystemConfig", + "L1CrossDomainMessenger", + "L1StandardBridge", + "OptimismPortal", + "L2OutputOracle", + "AddressManager", + "L1Block", + "L2ToL1MessagePasser", + "GasPriceOracle", + "L2CrossDomainMessenger", + "L2StandardBridge", + "L2ERC721Bridge", + "L1ERC721Bridge", + "OptimismMintableERC721Factory", + "SequencerFeeVault", + "BaseFeeVault", + "L1FeeVault", + "OptimismMintableERC20Factory", + "OptimismMintableERC20", + "LegacyERC20ETH", + "Proxy", + "ProxyAdmin", + "LegacyMessagePasser", + "ERC20", + "WETH9", + "DeployerWhitelist", + "L1BlockNumber", + "DisputeGameFactory", + "FaultDisputeGame", + "OutputBisectionGame", + "AlphabetVM", + "AlphabetVM2", + "StandardBridge", + "CrossDomainMessenger", + "MIPS", + "PreimageOracle", + "BlockOracle", + "EAS", + "SchemaRegistry", + "ProtocolVersions", + "Safe", + "SafeProxyFactory", + "DelayedVetoable", + "ISemver", + "StorageSetter", + "SuperchainConfig" + ] +} diff --git a/op-bindings/bindgen/generator_local.go b/op-bindings/bindgen/generator_local.go new file mode 100644 index 0000000000000..8f3a49e65ba1c --- /dev/null +++ b/op-bindings/bindgen/generator_local.go @@ -0,0 +1,253 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "text/template" + + "github.com/ethereum-optimism/optimism/op-bindings/ast" + "github.com/ethereum-optimism/optimism/op-bindings/foundry" +) + +type bindGenGeneratorLocal struct { + bindGenGeneratorBase + sourceMapsList string + forgeArtifactsPath string +} + +type localContractMetadata struct { + Name string + StorageLayout string + DeployedBin string + Package string + DeployedSourceMap string + HasImmutableReferences bool +} + +func (generator *bindGenGeneratorLocal) generateBindings() error { + contracts, err := readContractList(generator.logger, generator.contractsListPath) + if err != nil { + return fmt.Errorf("error reading contract list %s: %w", generator.contractsListPath, err) + } + if len(contracts.Local) == 0 { + return fmt.Errorf("no contracts parsed from given contract list: %s", generator.contractsListPath) + } + + return generator.processContracts(contracts.Local) +} + +func (generator *bindGenGeneratorLocal) processContracts(contracts []string) error { + tempArtifactsDir, err := mkTempArtifactsDir(generator.logger) + if err != nil { + return err + } + defer func() { + err := os.RemoveAll(tempArtifactsDir) + if err != nil { + generator.logger.Error("Error removing temporary artifact directory", "path", tempArtifactsDir, "err", err.Error()) + } else { + generator.logger.Debug("Successfully removed temporary artifact directory") + } + }() + + sourceMapsList := strings.Split(generator.sourceMapsList, ",") + sourceMapsSet := make(map[string]struct{}) + for _, k := range sourceMapsList { + sourceMapsSet[k] = struct{}{} + } + + contractArtifactPaths, err := generator.getContractArtifactPaths() + if err != nil { + return err + } + + contractMetadataFileTemplate := template.Must(template.New("localContractMetadata").Parse(localContractMetadataTemplate)) + + for _, contractName := range contracts { + generator.logger.Info("Generating bindings and metadata for local contract", "contract", contractName) + + forgeArtifact, err := generator.readForgeArtifact(contractName, contractArtifactPaths) + if err != nil { + return err + } + + abiFilePath, bytecodeFilePath, err := writeContractArtifacts(generator.logger, tempArtifactsDir, contractName, forgeArtifact.Abi, []byte(forgeArtifact.Bytecode.Object.String())) + if err != nil { + return err + } + + err = genContractBindings(generator.logger, abiFilePath, bytecodeFilePath, generator.bindingsPackageName, contractName) + if err != nil { + return err + } + + deployedSourceMap, canonicalStorageStr, err := generator.canonicalizeStorageLayout(forgeArtifact, sourceMapsSet, contractName) + if err != nil { + return err + } + + re := regexp.MustCompile(`\s+`) + immutableRefs, err := json.Marshal(re.ReplaceAllString(string(forgeArtifact.DeployedBytecode.ImmutableReferences), "")) + if err != nil { + return fmt.Errorf("error marshaling immutable references: %w", err) + } + + hasImmutables := string(immutableRefs) != `""` + + contractMetaData := localContractMetadata{ + Name: contractName, + StorageLayout: canonicalStorageStr, + DeployedBin: forgeArtifact.DeployedBytecode.Object.String(), + Package: generator.bindingsPackageName, + DeployedSourceMap: deployedSourceMap, + HasImmutableReferences: hasImmutables, + } + + if err := generator.writeContractMetadata(contractMetaData, contractName, contractMetadataFileTemplate); err != nil { + return err + } + } + + return nil +} + +func (generator *bindGenGeneratorLocal) getContractArtifactPaths() (map[string]string, error) { + // If some contracts have the same name then the path to their + // artifact depends on their full import path. Scan over all artifacts + // and hold a mapping from the contract name to the contract path. + // Walk walks the directory deterministically, so the earliest instance + // of the contract with the same name will be used + artifactPaths := make(map[string]string) + if err := filepath.Walk(generator.forgeArtifactsPath, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if strings.HasSuffix(path, ".json") { + base := filepath.Base(path) + name := strings.TrimSuffix(base, ".json") + + // remove the compiler version from the name + re := regexp.MustCompile(`\.\d+\.\d+\.\d+`) + sanitized := re.ReplaceAllString(name, "") + _, ok := artifactPaths[sanitized] + if !ok { + artifactPaths[sanitized] = path + } else { + generator.logger.Warn("Multiple versions of forge artifacts exist, using lesser version", "contract", sanitized) + } + } + return nil + }); err != nil { + return artifactPaths, err + } + + return artifactPaths, nil +} + +func (generator *bindGenGeneratorLocal) readForgeArtifact(contractName string, contractArtifactPaths map[string]string) (foundry.Artifact, error) { + var forgeArtifact foundry.Artifact + + contractArtifactPath := path.Join(generator.forgeArtifactsPath, contractName+".sol", contractName+".json") + forgeArtifactRaw, err := os.ReadFile(contractArtifactPath) + if errors.Is(err, os.ErrNotExist) { + generator.logger.Debug("Cannot find forge-artifact at standard path, trying provided path", "contract", contractName, "standardPath", contractArtifactPath, "providedPath", contractArtifactPaths[contractName]) + contractArtifactPath = contractArtifactPaths[contractName] + forgeArtifactRaw, err = os.ReadFile(contractArtifactPath) + if errors.Is(err, os.ErrNotExist) { + return forgeArtifact, fmt.Errorf("cannot find forge-artifact of %q", contractName) + } + } + + generator.logger.Debug("Using forge-artifact", "path", contractArtifactPath) + if err := json.Unmarshal(forgeArtifactRaw, &forgeArtifact); err != nil { + return forgeArtifact, fmt.Errorf("failed to parse forge artifact of %q: %w", contractName, err) + } + + return forgeArtifact, nil +} + +func (generator *bindGenGeneratorLocal) canonicalizeStorageLayout(forgeArtifact foundry.Artifact, sourceMapsSet map[string]struct{}, contractName string) (string, string, error) { + artifactStorageStruct := forgeArtifact.StorageLayout + canonicalStorageStruct := ast.CanonicalizeASTIDs(&artifactStorageStruct, generator.monorepoBasePath) + canonicalStorageJson, err := json.Marshal(canonicalStorageStruct) + if err != nil { + return "", "", fmt.Errorf("error marshaling canonical storage: %w", err) + } + canonicalStorageStr := strings.Replace(string(canonicalStorageJson), "\"", "\\\"", -1) + + deployedSourceMap := "" + if _, ok := sourceMapsSet[contractName]; ok { + deployedSourceMap = forgeArtifact.DeployedBytecode.SourceMap + } + + return deployedSourceMap, canonicalStorageStr, nil +} + +func (generator *bindGenGeneratorLocal) writeContractMetadata(contractMetaData localContractMetadata, contractName string, fileTemplate *template.Template) error { + metadataFilePath := filepath.Join(generator.metadataOut, strings.ToLower(contractName)+"_more.go") + metadataFile, err := os.OpenFile( + metadataFilePath, + os.O_RDWR|os.O_CREATE|os.O_TRUNC, + 0o600, + ) + if err != nil { + return fmt.Errorf("error opening %s's metadata file at %s: %w", contractName, metadataFilePath, err) + } + defer metadataFile.Close() + + if err := fileTemplate.Execute(metadataFile, contractMetaData); err != nil { + return fmt.Errorf("error writing %s's contract metadata at %s: %w", contractName, metadataFilePath, err) + } + + generator.logger.Debug("Successfully wrote contract metadata", "contract", contractName, "path", metadataFilePath) + return nil +} + +// associated with a local Ethereum contract. This template is used to produce +// Go code containing necessary constants and initialization logic for the contract's +// storage layout, deployed bytecode, and optionally its deployed source map. +// +// The template expects the following fields to be provided: +// - Package: The name of the Go package for the generated bindings. +// - Name: The name of the contract. +// - StorageLayout: Canonicalized storage layout of the contract as a JSON string. +// - DeployedBin: The deployed bytecode of the contract. +// - DeployedSourceMap (optional): The source map of the deployed contract. +var localContractMetadataTemplate = `// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package {{.Package}} + +import ( + "encoding/json" + + "github.com/ethereum-optimism/optimism/op-bindings/solc" +) + +const {{.Name}}StorageLayoutJSON = "{{.StorageLayout}}" + +var {{.Name}}StorageLayout = new(solc.StorageLayout) + +var {{.Name}}DeployedBin = "{{.DeployedBin}}" +{{if .DeployedSourceMap}} +var {{.Name}}DeployedSourceMap = "{{.DeployedSourceMap}}" +{{end}} + +func init() { + if err := json.Unmarshal([]byte({{.Name}}StorageLayoutJSON), {{.Name}}StorageLayout); err != nil { + panic(err) + } + + layouts["{{.Name}}"] = {{.Name}}StorageLayout + deployedBytecodes["{{.Name}}"] = {{.Name}}DeployedBin + immutableReferences["{{.Name}}"] = {{.HasImmutableReferences}} +} +` diff --git a/op-bindings/bindgen/main.go b/op-bindings/bindgen/main.go new file mode 100644 index 0000000000000..020633d088b1d --- /dev/null +++ b/op-bindings/bindgen/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "fmt" + "os" + + "github.com/ethereum-optimism/optimism/op-e2e/config" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum/go-ethereum/log" + "github.com/urfave/cli/v2" +) + +type bindGenGeneratorBase struct { + metadataOut string + bindingsPackageName string + monorepoBasePath string + contractsListPath string + logger log.Logger +} + +const ( + // Base Flags + MetadataOutFlagName = "metadata-out" + BindingsPackageNameFlagName = "bindings-package" + ContractsListFlagName = "contracts-list" + + // Local Contracts Flags + SourceMapsListFlagName = "source-maps-list" + ForgeArtifactsFlagName = "forge-artifacts" +) + +func main() { + oplog.SetupDefaults() + + app := &cli.App{ + Name: "BindGen", + Usage: "Generate contract bindings using Foundry artifacts and/or remotely sourced contract data", + Commands: []*cli.Command{ + { + Name: "generate", + Usage: "Generate contract bindings", + Flags: baseFlags(), + Subcommands: []*cli.Command{ + { + Name: "local", + Usage: "Generate bindings for locally sourced contracts", + Flags: localFlags(), + Action: generateBindings, + }, + }, + }, + }, + } + + if err := app.Run(os.Args); err != nil { + log.Crit("BindGen error", "error", err.Error()) + } +} + +func setupLogger(c *cli.Context) log.Logger { + logger := oplog.NewLogger(oplog.AppOut(c), oplog.ReadCLIConfig(c)) + oplog.SetGlobalLogHandler(logger.GetHandler()) + return logger +} + +func generateBindings(c *cli.Context) error { + logger := setupLogger(c) + + switch c.Command.Name { + case "local": + localBindingsGenerator, err := parseConfigLocal(logger, c) + if err != nil { + return err + } + if err := localBindingsGenerator.generateBindings(); err != nil { + return fmt.Errorf("error generating local bindings: %w", err) + } + return nil + default: + return fmt.Errorf("unknown command: %s", c.Command.Name) + } +} + +func parseConfigBase(logger log.Logger, c *cli.Context) (bindGenGeneratorBase, error) { + cwd, err := os.Getwd() + if err != nil { + return bindGenGeneratorBase{}, err + } + + monoRepoPath, err := config.FindMonorepoRoot(cwd) + if err != nil { + return bindGenGeneratorBase{}, err + } + + return bindGenGeneratorBase{ + metadataOut: c.String(MetadataOutFlagName), + bindingsPackageName: c.String(BindingsPackageNameFlagName), + monorepoBasePath: monoRepoPath, + contractsListPath: c.String(ContractsListFlagName), + logger: logger, + }, nil +} + +func parseConfigLocal(logger log.Logger, c *cli.Context) (bindGenGeneratorLocal, error) { + baseConfig, err := parseConfigBase(logger, c) + if err != nil { + return bindGenGeneratorLocal{}, err + } + return bindGenGeneratorLocal{ + bindGenGeneratorBase: baseConfig, + sourceMapsList: c.String(SourceMapsListFlagName), + forgeArtifactsPath: c.String(ForgeArtifactsFlagName), + }, nil +} + +func baseFlags() []cli.Flag { + baseFlags := []cli.Flag{ + &cli.StringFlag{ + Name: MetadataOutFlagName, + Usage: "Output directory to put contract metadata files in", + Required: true, + }, + &cli.StringFlag{ + Name: BindingsPackageNameFlagName, + Usage: "Go package name given to generated bindings", + Required: true, + }, + &cli.StringFlag{ + Name: ContractsListFlagName, + Usage: "Path to file containing list of contract names to generate bindings for", + Required: true, + }, + } + + return append(baseFlags, oplog.CLIFlags("bindgen")...) +} + +func localFlags() []cli.Flag { + return []cli.Flag{ + &cli.StringFlag{ + Name: SourceMapsListFlagName, + Usage: "Comma-separated list of contracts to generate source-maps for", + }, + &cli.StringFlag{ + Name: ForgeArtifactsFlagName, + Usage: "Path to forge-artifacts directory, containing compiled contract artifacts", + Required: true, + }, + } +} diff --git a/op-bindings/bindgen/utils.go b/op-bindings/bindgen/utils.go new file mode 100644 index 0000000000000..444b9305efcdf --- /dev/null +++ b/op-bindings/bindgen/utils.go @@ -0,0 +1,131 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path" + "strings" + + "github.com/ethereum/go-ethereum/log" +) + +type contractsList struct { + Local []string `json:"local"` +} + +// 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, 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") + 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) + } + + return nil +} diff --git a/op-bindings/gen/main.go b/op-bindings/gen/main.go deleted file mode 100644 index cb1bf1c10f8dd..0000000000000 --- a/op-bindings/gen/main.go +++ /dev/null @@ -1,241 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "flag" - "log" - "os" - "os/exec" - "path" - "path/filepath" - "regexp" - "strings" - "text/template" - - "github.com/ethereum-optimism/optimism/op-bindings/ast" - "github.com/ethereum-optimism/optimism/op-bindings/foundry" -) - -type flags struct { - ForgeArtifacts string - Contracts string - SourceMaps string - OutDir string - Package string - MonorepoBase string -} - -type data struct { - Name string - StorageLayout string - DeployedBin string - Package string - DeployedSourceMap string - HasImmutableReferences bool -} - -func main() { - var f flags - flag.StringVar(&f.ForgeArtifacts, "forge-artifacts", "", "Forge artifacts directory, to load sourcemaps from, if available") - flag.StringVar(&f.OutDir, "out", "", "Output directory to put code in") - flag.StringVar(&f.Contracts, "contracts", "artifacts.json", "Path to file containing list of contracts to generate bindings for") - flag.StringVar(&f.SourceMaps, "source-maps", "", "Comma-separated list of contracts to generate source-maps for") - flag.StringVar(&f.Package, "package", "artifacts", "Go package name") - flag.StringVar(&f.MonorepoBase, "monorepo-base", "", "Base of the monorepo") - flag.Parse() - - if f.MonorepoBase == "" { - log.Fatal("must provide -monorepo-base") - } - log.Printf("Using monorepo base %s\n", f.MonorepoBase) - - contractData, err := os.ReadFile(f.Contracts) - if err != nil { - log.Fatal("error reading contract list: %w\n", err) - } - contracts := []string{} - if err := json.Unmarshal(contractData, &contracts); err != nil { - log.Fatal("error parsing contract list: %w\n", err) - } - - sourceMaps := strings.Split(f.SourceMaps, ",") - sourceMapsSet := make(map[string]struct{}) - for _, k := range sourceMaps { - sourceMapsSet[k] = struct{}{} - } - - if len(contracts) == 0 { - log.Fatalf("must define a list of contracts") - } - - t := template.Must(template.New("artifact").Parse(tmpl)) - - // Make a temp dir to hold all the inputs for abigen - dir, err := os.MkdirTemp("", "op-bindings") - if err != nil { - log.Fatal(err) - } - log.Printf("Using package %s\n", f.Package) - - defer os.RemoveAll(dir) - log.Printf("created temp dir %s\n", dir) - - // If some contracts have the same name then the path to their - // artifact depends on their full import path. Scan over all artifacts - // and hold a mapping from the contract name to the contract path. - // Walk walks the directory deterministically, so the later instance - // of the contract with the same name will be used - re := regexp.MustCompile(`\.\d+\.\d+\.\d+`) - artifactPaths := make(map[string]string) - if err := filepath.Walk(f.ForgeArtifacts, - func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - if strings.HasSuffix(path, ".json") { - base := filepath.Base(path) - name := strings.TrimSuffix(base, ".json") - - // remove the compiler version from the name - sanitized := re.ReplaceAllString(name, "") - if _, ok := artifactPaths[sanitized]; !ok { - artifactPaths[sanitized] = path - } - } - return nil - }); err != nil { - log.Fatal(err) - } - - for _, name := range contracts { - log.Printf("generating code for %s\n", name) - - artifactPath := path.Join(f.ForgeArtifacts, name+".sol", name+".json") - forgeArtifactData, err := os.ReadFile(artifactPath) - if errors.Is(err, os.ErrNotExist) { - log.Printf("cannot find forge-artifact for %s at standard path %s, trying %s\n", name, artifactPath, artifactPaths[name]) - artifactPath = artifactPaths[name] - forgeArtifactData, err = os.ReadFile(artifactPath) - if errors.Is(err, os.ErrNotExist) { - log.Fatalf("cannot find forge-artifact of %q\n", name) - } - } - - log.Printf("using forge-artifact %s\n", artifactPath) - var artifact foundry.Artifact - if err := json.Unmarshal(forgeArtifactData, &artifact); err != nil { - log.Fatalf("failed to parse forge artifact of %q: %v\n", name, err) - } - - rawAbi := artifact.Abi - if err != nil { - log.Fatalf("error marshaling abi: %v\n", err) - } - abiFile := path.Join(dir, name+".abi") - if err := os.WriteFile(abiFile, rawAbi, 0o600); err != nil { - log.Fatalf("error writing file: %v\n", err) - } - rawBytecode := artifact.Bytecode.Object.String() - if err != nil { - log.Fatalf("error marshaling bytecode: %v\n", err) - } - bytecodeFile := path.Join(dir, name+".bin") - if err := os.WriteFile(bytecodeFile, []byte(rawBytecode), 0o600); err != nil { - log.Fatalf("error writing file: %v\n", err) - } - - cwd, err := os.Getwd() - if err != nil { - log.Fatalf("error getting cwd: %v\n", err) - } - - lowerName := strings.ToLower(name) - outFile := path.Join(cwd, f.Package, lowerName+".go") - - cmd := exec.Command("abigen", "--abi", abiFile, "--bin", bytecodeFile, "--pkg", f.Package, "--type", name, "--out", outFile) - cmd.Stdout = os.Stdout - - if err := cmd.Run(); err != nil { - log.Fatalf("error running abigen: %v\n", err) - } - - storage := artifact.StorageLayout - canonicalStorage := ast.CanonicalizeASTIDs(&storage, f.MonorepoBase) - ser, err := json.Marshal(canonicalStorage) - if err != nil { - log.Fatalf("error marshaling storage: %v\n", err) - } - serStr := strings.Replace(string(ser), "\"", "\\\"", -1) - - deployedSourceMap := "" - if _, ok := sourceMapsSet[name]; ok { - deployedSourceMap = artifact.DeployedBytecode.SourceMap - } - - re := regexp.MustCompile(`\s+`) - immutableRefs, err := json.Marshal(re.ReplaceAllString(string(artifact.DeployedBytecode.ImmutableReferences), "")) - if err != nil { - log.Fatalf("error marshaling immutable references: %v\n", err) - } - - hasImmutables := string(immutableRefs) != `""` - - d := data{ - Name: name, - StorageLayout: serStr, - DeployedBin: artifact.DeployedBytecode.Object.String(), - Package: f.Package, - DeployedSourceMap: deployedSourceMap, - HasImmutableReferences: hasImmutables, - } - - fname := filepath.Join(f.OutDir, strings.ToLower(name)+"_more.go") - outfile, err := os.OpenFile( - fname, - os.O_RDWR|os.O_CREATE|os.O_TRUNC, - 0o600, - ) - if err != nil { - log.Fatalf("error opening %s: %v\n", fname, err) - } - - if err := t.Execute(outfile, d); err != nil { - log.Fatalf("error writing template %s: %v", outfile.Name(), err) - } - outfile.Close() - log.Printf("wrote file %s\n", outfile.Name()) - } -} - -var tmpl = `// Code generated - DO NOT EDIT. -// This file is a generated binding and any manual changes will be lost. - -package {{.Package}} - -import ( - "encoding/json" - - "github.com/ethereum-optimism/optimism/op-bindings/solc" -) - -const {{.Name}}StorageLayoutJSON = "{{.StorageLayout}}" - -var {{.Name}}StorageLayout = new(solc.StorageLayout) - -var {{.Name}}DeployedBin = "{{.DeployedBin}}" -{{if .DeployedSourceMap}} -var {{.Name}}DeployedSourceMap = "{{.DeployedSourceMap}}" -{{end}} - -func init() { - if err := json.Unmarshal([]byte({{.Name}}StorageLayoutJSON), {{.Name}}StorageLayout); err != nil { - panic(err) - } - - layouts["{{.Name}}"] = {{.Name}}StorageLayout - deployedBytecodes["{{.Name}}"] = {{.Name}}DeployedBin - immutableReferences["{{.Name}}"] = {{.HasImmutableReferences}} -} -` diff --git a/op-e2e/config/init.go b/op-e2e/config/init.go index 80e21761b08cb..f481c7f105f12 100644 --- a/op-e2e/config/init.go +++ b/op-e2e/config/init.go @@ -49,7 +49,7 @@ func init() { if err != nil { panic(err) } - root, err := findMonorepoRoot(cwd) + root, err := FindMonorepoRoot(cwd) if err != nil { panic(err) } @@ -159,9 +159,9 @@ func allExist(filenames ...string) error { return nil } -// findMonorepoRoot will recursively search upwards for a go.mod file. +// FindMonorepoRoot will recursively search upwards for a go.mod file. // This depends on the structure of the monorepo having a go.mod file at the root. -func findMonorepoRoot(startDir string) (string, error) { +func FindMonorepoRoot(startDir string) (string, error) { dir, err := filepath.Abs(startDir) if err != nil { return "", err