Skip to content
Draft
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
270 changes: 270 additions & 0 deletions cmd/generate-bindings/evm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
package generatebindings

import (
"fmt"
"os"
"path/filepath"

"github.com/spf13/viper"

"github.com/smartcontractkit/cre-cli/cmd/creinit"
"github.com/smartcontractkit/cre-cli/cmd/generate-bindings/evm"
"github.com/smartcontractkit/cre-cli/internal/validation"
)

func resolveEvmInputs(args []string, v *viper.Viper) (EvmInputs, error) {
// Get current working directory as default project root
currentDir, err := os.Getwd()
if err != nil {
return EvmInputs{}, fmt.Errorf("failed to get current working directory: %w", err)
}

// Resolve project root with fallback to current directory
projectRoot := v.GetString("project-root")
if projectRoot == "" {
projectRoot = currentDir
}

contractsPath := filepath.Join(projectRoot, "contracts")
if _, err := os.Stat(contractsPath); err != nil {
return EvmInputs{}, fmt.Errorf("contracts folder not found in project root: %s", contractsPath)
}

// Chain family is now a positional argument
chainFamily := args[0]

// Language defaults are handled by StringP
language := v.GetString("language")

// Resolve ABI path with fallback to contracts/{chainFamily}/src/abi/
abiPath := v.GetString("abi")
if abiPath == "" {
abiPath = filepath.Join(projectRoot, "contracts", chainFamily, "src", "abi")
}

// Package name defaults are handled by StringP
pkgName := v.GetString("pkg")

// Output path is contracts/{chainFamily}/src/generated/ under projectRoot
outPath := filepath.Join(projectRoot, "contracts", chainFamily, "src", "generated")

return EvmInputs{
ProjectRoot: projectRoot,
ChainFamily: chainFamily,
Language: language,
AbiPath: abiPath,
PkgName: pkgName,
OutPath: outPath,
}, nil
}

func validateEvmInputs(inputs EvmInputs) error {
validate, err := validation.NewValidator()
if err != nil {
return fmt.Errorf("failed to initialize validator: %w", err)
}

if err = validate.Struct(inputs); err != nil {
return validate.ParseValidationErrors(err)
}

// Additional validation for ABI path
if _, err := os.Stat(inputs.AbiPath); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("ABI path does not exist: %s", inputs.AbiPath)
}
return fmt.Errorf("failed to access ABI path: %w", err)
}

// Validate that if AbiPath is a directory, it contains .abi files
if info, err := os.Stat(inputs.AbiPath); err == nil && info.IsDir() {
files, err := filepath.Glob(filepath.Join(inputs.AbiPath, "*.abi"))
if err != nil {
return fmt.Errorf("failed to check for ABI files in directory: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("no .abi files found in directory: %s", inputs.AbiPath)
}
}

return nil
}

// contractNameToPackage converts contract names to valid Go package names
// Examples: IERC20 -> ierc20, ReserveManager -> reserve_manager, IReserveManager -> ireserve_manager
func contractNameToPackage(contractName string) string {
if contractName == "" {
return ""
}

var result []rune
runes := []rune(contractName)

for i, r := range runes {
// Convert to lowercase
if r >= 'A' && r <= 'Z' {
lower := r - 'A' + 'a'

// Add underscore before uppercase letters, but not:
// - At the beginning (i == 0)
// - If the previous character was also uppercase and this is followed by lowercase (e.g., "ERC" in "ERC20")
// - If this is part of a sequence of uppercase letters at the beginning (e.g., "IERC20" -> "ierc20")
if i > 0 {
prevIsUpper := runes[i-1] >= 'A' && runes[i-1] <= 'Z'
nextIsLower := i+1 < len(runes) && runes[i+1] >= 'a' && runes[i+1] <= 'z'

// Add underscore if:
// - Previous char was lowercase (CamelCase boundary)
// - Previous char was uppercase but this char is followed by lowercase (end of acronym)
if !prevIsUpper || (prevIsUpper && nextIsLower && i > 1) {
result = append(result, '_')
}
}

result = append(result, lower)
} else {
result = append(result, r)
}
}

return string(result)
}

func processEvmAbiDirectory(inputs EvmInputs) error {
// Read all .abi files in the directory
files, err := filepath.Glob(filepath.Join(inputs.AbiPath, "*.abi"))
if err != nil {
return fmt.Errorf("failed to find ABI files: %w", err)
}

if len(files) == 0 {
return fmt.Errorf("no .abi files found in directory: %s", inputs.AbiPath)
}

packageNames := make(map[string]bool)
for _, abiFile := range files {
contractName := filepath.Base(abiFile)
contractName = contractName[:len(contractName)-4]
packageName := contractNameToPackage(contractName)
if _, exists := packageNames[packageName]; exists {
return fmt.Errorf("package name collision: multiple contracts would generate the same package name '%s' (contracts are converted to snake_case for package names). Please rename one of your contract files to avoid this conflict", packageName)
}
packageNames[packageName] = true
}

// Process each ABI file
for _, abiFile := range files {
// Extract contract name from filename (remove .abi extension)
contractName := filepath.Base(abiFile)
contractName = contractName[:len(contractName)-4] // Remove .abi extension

// Convert contract name to package name
packageName := contractNameToPackage(contractName)

// Create per-contract output directory
contractOutDir := filepath.Join(inputs.OutPath, packageName)
if err := os.MkdirAll(contractOutDir, 0o755); err != nil {
return fmt.Errorf("failed to create contract output directory %s: %w", contractOutDir, err)
}

// Create output file path in contract-specific directory
outputFile := filepath.Join(contractOutDir, contractName+".go")

fmt.Printf("Processing ABI file: %s, contract: %s, package: %s, output: %s\n", abiFile, contractName, packageName, outputFile)

err = evm.GenerateBindings(
"", // combinedJSONPath - empty for now
abiFile,
packageName, // Use contract-specific package name
contractName, // Use contract name as type name
outputFile,
)
if err != nil {
return fmt.Errorf("failed to generate bindings for %s: %w", contractName, err)
}
}

return nil
}

func processEvmSingleAbi(inputs EvmInputs) error {
// Extract contract name from ABI file path
contractName := filepath.Base(inputs.AbiPath)
if filepath.Ext(contractName) == ".abi" {
contractName = contractName[:len(contractName)-4] // Remove .abi extension
}

// Convert contract name to package name
packageName := contractNameToPackage(contractName)

// Create per-contract output directory
contractOutDir := filepath.Join(inputs.OutPath, packageName)
if err := os.MkdirAll(contractOutDir, 0o755); err != nil {
return fmt.Errorf("failed to create contract output directory %s: %w", contractOutDir, err)
}

// Create output file path in contract-specific directory
outputFile := filepath.Join(contractOutDir, contractName+".go")

fmt.Printf("Processing single ABI file: %s, contract: %s, package: %s, output: %s\n", inputs.AbiPath, contractName, packageName, outputFile)

return evm.GenerateBindings(
"", // combinedJSONPath - empty for now
inputs.AbiPath,
packageName, // Use contract-specific package name
contractName, // Use contract name as type name
outputFile,
)
}

func executeEvm(inputs EvmInputs) error {
fmt.Printf("GenerateBindings would be called here: projectRoot=%s, chainFamily=%s, language=%s, abiPath=%s, pkgName=%s, outPath=%s\n", inputs.ProjectRoot, inputs.ChainFamily, inputs.Language, inputs.AbiPath, inputs.PkgName, inputs.OutPath)

// Validate language
switch inputs.Language {
case "go":
// Language supported, continue
default:
return fmt.Errorf("unsupported language: %s", inputs.Language)
}

// Validate chain family and handle accordingly
switch inputs.ChainFamily {
case "evm":
// Create output directory if it doesn't exist
if err := os.MkdirAll(inputs.OutPath, 0o755); err != nil {
return fmt.Errorf("failed to create output directory: %w", err)
}

// Check if ABI path is a directory or file
info, err := os.Stat(inputs.AbiPath)
if err != nil {
return fmt.Errorf("failed to access ABI path: %w", err)
}

if info.IsDir() {
if err := processEvmAbiDirectory(inputs); err != nil {
return err
}
} else {
if err := processEvmSingleAbi(inputs); err != nil {
return err
}
}

err = runCommand(inputs.ProjectRoot, "go", "get", "github.com/smartcontractkit/cre-sdk-go@"+creinit.SdkVersion)
if err != nil {
return err
}
err = runCommand(inputs.ProjectRoot, "go", "get", "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm@"+creinit.EVMCapabilitiesVersion)
if err != nil {
return err
}
if err = runCommand(inputs.ProjectRoot, "go", "mod", "tidy"); err != nil {
return err
}
return nil
default:
return fmt.Errorf("unsupported chain family: %s", inputs.ChainFamily)
}
}
49 changes: 49 additions & 0 deletions cmd/generate-bindings/evm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## License

This repository contains two separate license regimes:

1. **LGPL-3.0-or-later** for all code in `./abigen` (the forked go-ethereum abigen).
See the full text in `LICENSE` under “GNU LESSER…”
2. **MIT** for everything else in this repo.
See the full text in `LICENSE` under “MIT License”.


# CRE Generated Bindings (MVP)

This project utilizes a forked version of `abigen` (from go-ethereum)
that lets you generate Go bindings for your smart contracts using a custom template.

## Prerequisites

1. **Go**
Install Go 1.18 or later:
```bash
brew install go # macOS (Homebrew)
sudo apt install golang # Ubuntu/Debian
```
2. **Solidity compiler**
Install `solc` to compile or verify your contracts:
```bash
npm install -g solc # via npm
brew install solidity # macOS (Homebrew)
```

## Usage
### Programmatic API

```go
import "github.com/smartcontractkit/cre-cli/cmd/generate-bindings/evm"

func main() {
err := bindings.GenerateBindings(
"./pkg/bindings/build/MyContract_combined.json", // or "" if using abiPath
"./pkg/bindings/MyContract.abi", // or "" for combined-json mode
"bindings", // Go package name
"MyContract", // typeName (single-ABI only)
"./pkg/bindings/build/bindings.go", // output file
)
if err != nil {
log.Fatalf("generate bindings: %v", err)
}
}
```
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package bindings
package evm

import (
_ "embed"
Expand All @@ -11,7 +11,7 @@ import (
"github.com/ethereum/go-ethereum/common/compiler"
"github.com/ethereum/go-ethereum/crypto"

"github.com/smartcontractkit/cre-cli/cmd/generate-bindings/bindings/abigen"
"github.com/smartcontractkit/cre-cli/cmd/generate-bindings/evm/abigen"
)

//go:embed sourcecre.go.tpl
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package bindings_test
package evm_test

import (
"context"
Expand All @@ -20,7 +20,7 @@
"github.com/smartcontractkit/cre-sdk-go/cre/testutils"
consensusmock "github.com/smartcontractkit/cre-sdk-go/internal_testing/capabilities/consensus/mock"

datastorage "github.com/smartcontractkit/cre-cli/cmd/generate-bindings/bindings/testdata"
datastorage "github.com/smartcontractkit/cre-cli/cmd/generate-bindings/evm/testdata"

Check failure on line 23 in cmd/generate-bindings/evm/bindings_test.go

View workflow job for this annotation

GitHub Actions / ci-test-unit

found packages bindings (bindings.go) and bindingsold (bindingsold.go) in /home/runner/work/cre-cli/cre-cli/cmd/generate-bindings/evm/testdata
)

const anyChainSelector = uint64(1337)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
//go:generate go run ./testdata/gen
package bindings
package evm
31 changes: 31 additions & 0 deletions cmd/generate-bindings/evm/gen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package evm_test

import (
"testing"

"github.com/smartcontractkit/cre-cli/cmd/generate-bindings/evm"
)

func TestGenerateBindings(t *testing.T) {
if err := evm.GenerateBindings(
"./testdata/DataStorage_combined.json",
"",
"bindings",
"",
"./testdata/bindings.go",
); err != nil {
t.Fatal(err)
}
}

func TestGenerateBindingsOld(t *testing.T) {
if err := evm.GenerateBindings(
"./testdata/DataStorage_combined.json",
"",
"bindingsold",
"",
"./testdata/bindingsold.go",
); err != nil {
t.Fatal(err)
}
}
Loading
Loading