Skip to content
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": minor
---

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.
13 changes: 11 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,15 @@ 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) {
b := binder{
contracts: make(map[string]*tmplContractV2),
structs: make(map[string]*tmplStruct),
Expand Down Expand Up @@ -562,7 +568,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, options}); err != nil {
return "", err
}
return buffer.String(), nil
Expand Down
10 changes: 8 additions & 2 deletions 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 with zero-valued options.
// Enable IncludeFunctionSelector 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 All @@ -175,7 +181,7 @@ func GenerateBindingsTS(
return fmt.Errorf("write %q: %w", outPath, err)
}

mockSrc, err := abigen.BindV2TS(types, abis, bins, "", libs, aliases, tsMockTpl)
mockSrc, err := abigen.BindV2TS(types, abis, bins, "", libs, aliases, tsMockTpl, options)
if err != nil {
return fmt.Errorf("BindV2TS mock: %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, TSBindingOptions{})
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, "getFunctionBySignature(PriceUpdaterABI, 'updatePrices((uint256,uint256))').inputs")
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.Contains(t, src, "abi: [getFunctionBySignature(CounterABI, 'setValue(uint256)')]")
} else {
assert.Contains(t, src, "const encodedPayload = encodeAbiParameters(")
assert.Contains(t, src, "getFunctionBySignature(CounterABI, 'setValue(uint256)').inputs")
}
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