synapsecns/sanguine

View on GitHub
tools/abigen/internal/generate.go

Summary

Maintainability
B
5 hrs
Test Coverage
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
}