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
5 changes: 5 additions & 0 deletions .changeset/evm-bindings-report-payload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"cre-cli": major
---

Breaking change: generated TypeScript EVM `writeReportFrom*` helpers now sign only ABI-encoded arguments by default, excluding the 4-byte function selector. If your receiver expects the legacy full-calldata payload, regenerate your bindings with `cre generate-bindings evm --include-function-selector`. Go bindings are unaffected.
17 changes: 15 additions & 2 deletions cmd/generate-bindings/evm/abigen/bindv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,19 @@ func tsBindType(kind abi.Type, structs map[string]*tmplStruct) string {
}
}

// TSBindingOptions controls TypeScript binding generation.
type TSBindingOptions struct {
// IncludeFunctionSelector restores legacy full-calldata report payloads.
IncludeFunctionSelector bool
}

// BindV2TS generates TypeScript bindings using the same ABI parsing as BindV2
// but with TypeScript-specific template functions and no Go formatting.
func BindV2TS(types []string, abis []string, bytecodes []string, pkg string, libs map[string]string, aliases map[string]string, templateContent string) (string, error) {
func BindV2TS(types []string, abis []string, bytecodes []string, pkg string, libs map[string]string, aliases map[string]string, templateContent string, options ...TSBindingOptions) (string, error) {
var opts TSBindingOptions
if len(options) > 0 {
opts = options[0]
}
b := binder{
contracts: make(map[string]*tmplContractV2),
structs: make(map[string]*tmplStruct),
Expand Down Expand Up @@ -562,7 +572,10 @@ func BindV2TS(types []string, abis []string, bytecodes []string, pkg string, lib
},
}
tmpl := template.Must(template.New("").Funcs(funcs).Parse(templateContent))
if err := tmpl.Execute(buffer, data); err != nil {
if err := tmpl.Execute(buffer, struct {
tmplDataV2
TSBindingOptions
}{data, opts}); err != nil {
return "", err
}
return buffer.String(), nil
Expand Down
8 changes: 7 additions & 1 deletion cmd/generate-bindings/evm/bindgen.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,16 @@ func GenerateBindings(
return nil
}

// TSBindingOptions controls TypeScript binding generation.
type TSBindingOptions = abigen.TSBindingOptions

// GenerateBindingsTS generates args-only report helpers by default. Pass options
// with IncludeFunctionSelector enabled to restore legacy full-calldata reports.
func GenerateBindingsTS(
abiPath string,
typeName string,
outPath string,
options ...TSBindingOptions,
) error {
if abiPath == "" {
return errors.New("must provide abiPath")
Expand All @@ -166,7 +172,7 @@ func GenerateBindingsTS(
libs := make(map[string]string)
aliases := make(map[string]string)

outSrc, err := abigen.BindV2TS(types, abis, bins, "", libs, aliases, tsTpl)
outSrc, err := abigen.BindV2TS(types, abis, bins, "", libs, aliases, tsTpl, options...)
if err != nil {
return fmt.Errorf("BindV2TS: %w", err)
}
Expand Down
39 changes: 24 additions & 15 deletions cmd/generate-bindings/evm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@ import (
)

type Inputs struct {
ProjectRoot string `validate:"required,dir" cli:"--project-root"`
GoLang bool
TypeScript bool
AbiPath string `validate:"required,path_read" cli:"--abi"`
PkgName string `validate:"required" cli:"--pkg"`
GoOutPath string // contracts/evm/src/generated — set when GoLang is true
TSOutPath string // contracts/evm/ts/generated — set when TypeScript is true
ProjectRoot string `validate:"required,dir" cli:"--project-root"`
GoLang bool
TypeScript bool
IncludeFunctionSelector bool
AbiPath string `validate:"required,path_read" cli:"--abi"`
PkgName string `validate:"required" cli:"--pkg"`
GoOutPath string // contracts/evm/src/generated — set when GoLang is true
TSOutPath string // contracts/evm/ts/generated — set when TypeScript is true
}

func New(runtimeContext *runtime.Context) *cobra.Command {
Expand All @@ -40,7 +41,11 @@ Each contract gets its own package subdirectory to avoid naming conflicts.
For example, IERC20.abi generates bindings in generated/ierc20/ package.

Both raw ABI files (*.abi) and JSON artifact files (*.json) are supported.
For JSON files the ABI is read from the top-level "abi" field.`,
For JSON files the ABI is read from the top-level "abi" field.

TypeScript report helpers sign ABI-encoded arguments by default.
Use --include-function-selector to restore legacy full-calldata reports
for receivers that expect the 4-byte function selector. Go bindings are unaffected.`,
Example: " cre generate-bindings evm",
RunE: func(cmd *cobra.Command, args []string) error {
handler := newHandler(runtimeContext)
Expand All @@ -60,6 +65,7 @@ For JSON files the ABI is read from the top-level "abi" field.`,
generateBindingsCmd.Flags().StringP("language", "l", "", "Target language: go, typescript (auto-detected from project files when omitted)")
generateBindingsCmd.Flags().StringP("abi", "a", "", "Path to ABI directory (defaults to contracts/evm/src/abi/). Supports *.abi and *.json files")
generateBindingsCmd.Flags().StringP("pkg", "k", "bindings", "Base package name (each contract gets its own subdirectory)")
generateBindingsCmd.Flags().Bool("include-function-selector", false, "Include the function selector in TypeScript report payloads (legacy behavior)")

return generateBindingsCmd
}
Expand Down Expand Up @@ -150,13 +156,14 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) {
}

return Inputs{
ProjectRoot: projectRoot,
GoLang: goLang,
TypeScript: typescript,
AbiPath: abiPath,
PkgName: pkgName,
GoOutPath: goOutPath,
TSOutPath: tsOutPath,
ProjectRoot: projectRoot,
GoLang: goLang,
TypeScript: typescript,
IncludeFunctionSelector: v.GetBool("include-function-selector"),
AbiPath: abiPath,
PkgName: pkgName,
GoOutPath: goOutPath,
TSOutPath: tsOutPath,
}, nil
}

Expand Down Expand Up @@ -305,6 +312,7 @@ func (h *handler) processAbiDirectory(inputs Inputs) error {
abiFile,
contractName,
outputFile,
TSBindingOptions{IncludeFunctionSelector: inputs.IncludeFunctionSelector},
)
if err != nil {
return fmt.Errorf("failed to generate TypeScript bindings for %s: %w", contractName, err)
Expand Down Expand Up @@ -363,6 +371,7 @@ func (h *handler) processSingleAbi(inputs Inputs) error {
inputs.AbiPath,
contractName,
outputFile,
TSBindingOptions{IncludeFunctionSelector: inputs.IncludeFunctionSelector},
); err != nil {
return err
}
Expand Down
104 changes: 104 additions & 0 deletions cmd/generate-bindings/evm/evm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/rs/zerolog"
Expand Down Expand Up @@ -386,6 +387,109 @@ func TestEndToEnd_TypeScriptGeneration(t *testing.T) {
require.FileExists(t, filepath.Join(tsOutDir, "index.ts"))
}

func TestGenerateBindingsTS_WriteReportHelperEncodesArgsOnly(t *testing.T) {
tempDir, err := os.MkdirTemp("", "generate-bindings-ts-report-test")
require.NoError(t, err)
defer os.RemoveAll(tempDir)

abiContent := `[
{
"type": "function",
"name": "updatePrices",
"inputs": [{
"name": "priceData",
"type": "tuple",
"internalType": "struct PriceUpdater.PriceData",
"components": [
{"name": "ethPrice", "type": "uint256"},
{"name": "btcPrice", "type": "uint256"}
]
}],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "latestPrice",
"inputs": [],
"outputs": [{"name": "", "type": "uint256"}],
"stateMutability": "view"
}
]`

abiFile := filepath.Join(tempDir, "PriceUpdater.abi")
err = os.WriteFile(abiFile, []byte(abiContent), 0600)
require.NoError(t, err)

outFile := filepath.Join(tempDir, "PriceUpdater.ts")
err = GenerateBindingsTS(abiFile, "PriceUpdater", outFile)
require.NoError(t, err)

content, err := os.ReadFile(outFile)
require.NoError(t, err)
src := string(content)

assert.Contains(t, src, "encodeAbiParameters")
assert.Contains(t, src, "getFunctionInputs(PriceUpdaterABI, 'updatePrices')")
assert.Contains(t, src, ".report(prepareReportRequest(encodedPayload))")
assert.Contains(t, src, "const callData = encodeFunctionData({")
assert.Equal(t, 1, strings.Count(src, "const callData = encodeFunctionData({"), "only view calls should build calldata in this fixture")
}

func TestIncludeFunctionSelector(t *testing.T) {
for _, mode := range []struct {
name string
flags []string
includeSelector bool
}{
{name: "default"},
{name: "legacy", flags: []string{"--include-function-selector"}, includeSelector: true},
{name: "explicit false", flags: []string{"--include-function-selector=false"}},
} {
for _, singleFile := range []bool{false, true} {
t.Run(fmt.Sprintf("%s/singleFile=%t", mode.name, singleFile), func(t *testing.T) {
projectRoot := t.TempDir()
abiDir := filepath.Join(projectRoot, "contracts", "evm", "src", "abi")
require.NoError(t, os.MkdirAll(abiDir, 0o755))
abiFile := filepath.Join(abiDir, "Counter.abi")
require.NoError(t, os.WriteFile(abiFile, []byte(`[
{"type":"function","name":"setValue","inputs":[{"name":"value","type":"uint256"}],"outputs":[],"stateMutability":"nonpayable"},
{"type":"function","name":"getValue","inputs":[],"outputs":[{"name":"","type":"uint256"}],"stateMutability":"view"}
]`), 0o600))
v := viper.New()
ctx := &runtime.Context{Viper: v}
cmd := New(ctx)
flags := append([]string{"--project-root", projectRoot, "--language", "typescript"}, mode.flags...)
if singleFile {
flags = append(flags, "--abi", abiFile)
}
require.NoError(t, cmd.ParseFlags(flags))
require.NoError(t, v.BindPFlags(cmd.Flags()))
inputs, err := newHandler(ctx).ResolveInputs(v)
require.NoError(t, err)
require.Equal(t, mode.includeSelector, inputs.IncludeFunctionSelector)
require.NoError(t, cmd.RunE(cmd, nil))

content, err := os.ReadFile(filepath.Join(inputs.TSOutPath, "Counter.ts"))
require.NoError(t, err)
src := string(content)
if mode.includeSelector {
assert.Contains(t, src, "const encodedPayload = encodeFunctionData({")
assert.NotContains(t, src, "encodeAbiParameters")
assert.NotContains(t, src, "getFunctionInputs")
} else {
assert.Contains(t, src, "const encodedPayload = encodeAbiParameters(")
assert.Contains(t, src, "getFunctionInputs(CounterABI, 'setValue')")
}
assert.Contains(t, src, ".report(prepareReportRequest(encodedPayload))")
assert.Equal(t, 1, strings.Count(src, "const callData = encodeFunctionData({"), "view calls must retain their selector")
assert.Contains(t, src, ".report(prepareReportRequest(callData))", "raw writeReport payloads must remain unchanged")
require.FileExists(t, filepath.Join(inputs.TSOutPath, "Counter_mock.ts"))
})
}
}
}

func TestResolveEvmInputs_CustomProjectRoot(t *testing.T) {
tempDir, err := os.MkdirTemp("", "generate-bindings-test")
require.NoError(t, err)
Expand Down
Loading
Loading