Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions op-bindings/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down
98 changes: 50 additions & 48 deletions op-bindings/artifacts.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
253 changes: 253 additions & 0 deletions op-bindings/bindgen/generator_local.go
Original file line number Diff line number Diff line change
@@ -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}}
}
`
Loading