tools/abigen/internal/generate.go
package internal
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strconv"
"strings"
"text/template"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/compiler"
"github.com/ethereum/go-ethereum/crypto"
"github.com/synapsecns/sanguine/tools/abigen/internal/etherscan"
)
// GenerateABIFromEtherscan generates the abi for an etherscan file.
func GenerateABIFromEtherscan(ctx context.Context, chainID uint32, url string, contractAddress common.Address, fileName, solVersion, pkgName string) error {
client, err := etherscan.NewEtherscanAbiGenClientFromChain(ctx, chainID, url)
if err != nil {
return fmt.Errorf("could not etherscan client for chain %d", chainID)
}
contractSource, err := client.ContractSource(contractAddress.String())
if err != nil {
return fmt.Errorf("could not get contract source for address %s: %w", contractAddress, err)
}
solFile, err := os.Create(fmt.Sprintf("%s/%s.sol", os.TempDir(), path.Base(fileName)))
if err != nil {
return fmt.Errorf("could not determine wd: %w", err)
}
defer func() {
_ = solFile.Close()
_ = os.RemoveAll(solFile.Name())
}()
var optimizerRuns int
for _, contract := range contractSource {
_, err = solFile.WriteString(contract.SourceCode)
if err != nil {
return fmt.Errorf("could not write to temporary sol file %s: %w", solFile.Name(), err)
}
optimizerRuns = contract.Runs
}
return BuildTemplates(solVersion, solFile.Name(), pkgName, fileName, optimizerRuns, nil)
}
// BuildTemplates builds the templates. version is the solidity version to use and sol is the solidity file to use.
func BuildTemplates(version, file, pkg, filename string, optimizerRuns int, evmVersion *string) error {
// TODO ast
contracts, err := compileSolidity(version, file, optimizerRuns, evmVersion)
if err != nil {
return err
}
marshalledContracts, err := json.Marshal(&contracts)
if err != nil {
return fmt.Errorf("could not marshall contracts: %w", err)
}
var (
abis []string
bins []string
types []string
sigs []map[string]string
libs = make(map[string]string)
aliases = make(map[string]string)
)
// Gather all non-excluded contract for binding
for name, contract := range contracts {
abi, err := json.Marshal(contract.Info.AbiDefinition) // Flatten the compiler parse
if err != nil {
panic(fmt.Errorf("failed to parse ABIs from compiler output: %w", err))
}
abis = append(abis, string(abi))
bins = append(bins, contract.Code)
sigs = append(sigs, contract.Hashes)
nameParts := strings.Split(name, ":")
types = append(types, nameParts[len(nameParts)-1])
libPattern := crypto.Keccak256Hash([]byte(name)).String()[2:36]
libs[libPattern] = nameParts[len(nameParts)-1]
}
code, err := bind.Bind(types, abis, bins, sigs, pkg, bind.LangGo, libs, aliases)
if err != nil {
return fmt.Errorf("could not generate abigen file: %w", err)
}
err = os.WriteFile(fmt.Sprintf("%s.abigen.go", filename), []byte(code), 0600)
if err != nil {
return fmt.Errorf("could not write abigen file: %w", err)
}
err = os.WriteFile(fmt.Sprintf("%s.contractinfo.json", filename), marshalledContracts, 0600)
if err != nil {
return fmt.Errorf("could not write contract info file: %w", err)
}
f, err := os.Create(fmt.Sprintf("%s.metadata.go", filename))
if err != nil {
return fmt.Errorf("could not create metadata file: %w", err)
}
_ = metadataTemplate.Execute(f, struct {
PackageName string
Name string
}{
PackageName: pkg,
Name: filename,
})
return nil
}
// compileSolidity uses docker to compile solidity.
// nolint: cyclop
func compileSolidity(version string, filePath string, optimizeRuns int, evmVersion *string) (map[string]*compiler.Contract, error) {
runFile, err := createRunFile(version)
if err != nil {
return nil, err
}
_ = runFile.Close()
wd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("could not determine working dir: %w", err)
}
//nolint: gosec
solContents, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("could not read sol file %s: %w", filePath, err)
}
// create a temporary sol file in the current dir so it can be referenced by docker
tmpPath := fmt.Sprintf("%s/%s", wd, path.Base(filePath))
var solFile *os.File
// whether the original file is at the temporary path already
originalAtTmpPath, err := filePathsAreEqual(tmpPath, filePath)
if err != nil {
return nil, fmt.Errorf("could not compare file paths: %w", err)
}
// we don't need to create a temporary file if it's already in our path!
//nolint: nestif
if !originalAtTmpPath {
//nolint: gosec
solFile, err = os.Create(tmpPath)
if err != nil {
return nil, fmt.Errorf("could not create temporary sol file: %w", err)
}
_, err = solFile.Write(solContents)
if err != nil {
return nil, fmt.Errorf("could not write to sol tmp file at %s: %w", solFile.Name(), err)
}
defer func() {
if err == nil {
err = os.Remove(solFile.Name())
} else {
_ = os.Remove(solFile.Name())
}
}()
} else {
// nolint: gosec
solFile, err = os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("could not read to sol file at %s: %w", solFile.Name(), err)
}
}
// compile the solidity
var stderr, stdout bytes.Buffer
args := []string{"--combined-json", "bin,bin-runtime,srcmap,srcmap-runtime,abi,userdoc,devdoc,metadata,hashes", "--optimize", "--optimize-runs", strconv.Itoa(optimizeRuns), "--allow-paths", "., ./, ../"}
if evmVersion != nil {
args = append(args, fmt.Sprintf("--evm-version=%s", *evmVersion))
}
//nolint: gosec
cmd := exec.Command(runFile.Name(), append(args, "--", fmt.Sprintf("/solidity/%s", filepath.Base(solFile.Name())))...)
cmd.Stderr = &stderr
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("solc: %w\n%s", err, stderr.Bytes())
}
contract, err := compiler.ParseCombinedJSON(stdout.Bytes(), string(solContents), version, version, strings.Join(args, " "))
if err != nil {
return nil, fmt.Errorf("could not parse json: %w", err)
}
return contract, nil
}
// createRunFile creates a bash file to run a command in the specified version of solidity.
func createRunFile(version string) (runFile *os.File, err error) {
runFile, err = os.CreateTemp("", "*")
if err != nil {
return nil, fmt.Errorf("could not create temp file: %w", err)
}
// create a bash file that runs solidity with args passed to the run file
_, err = runFile.WriteString(fmt.Sprintf("#!/bin/bash -e \n$(which docker) run --platform linux/amd64 -v $(pwd):/solidity ethereum/solc:%s \"$@\"", version))
if err != nil {
return nil, fmt.Errorf("could not create temp file: %w", err)
}
// TODO this should really be done natively. See: https://pkg.go.dev/bitbucket.org/dchapes/mode maybe?
//nolint: gosec
err = exec.Command("chmod", "+x", runFile.Name()).Run()
if err != nil {
return nil, fmt.Errorf("could not make sol runner executable")
}
return runFile, nil
}
// TODO consider only building on test, as these contracts *will* get included into production binaries.
var metadataTemplate = template.Must(template.New("").Parse(`// Code generated by synapse abigen DO NOT EDIT.
package {{ .PackageName }}
import (
_ "embed"
"encoding/json"
"github.com/ethereum/go-ethereum/common/compiler"
)
// rawContracts are the json we use to derive the processed contracts
//
//go:embed {{ .Name }}.contractinfo.json
var rawContracts []byte
// Contracts are unmarshalled on start
var Contracts map[string]*compiler.Contract
func init() {
// load contract metadata
var err error
err = json.Unmarshal(rawContracts, &Contracts)
if err != nil {
panic(err)
}
}
`))
func filePathsAreEqual(file1 string, file2 string) (equal bool, err error) {
// get the absolute path of the files
file1, err = filepath.Abs(file1)
if err != nil {
return false, fmt.Errorf("could not get absolute path of %s: %w", file1, err)
}
file2, err = filepath.Abs(file2)
if err != nil {
return false, fmt.Errorf("could not get absolute path of %s: %w", file2, err)
}
// check if the files are the same
return file1 == file2, nil
}