From 6ffab58727fa019ec4957b378392a16759aff7c0 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 11 Sep 2026 14:26:42 -0400 Subject: [PATCH 1/7] EVM Bindings: Sign args only --- cmd/generate-bindings/evm/evm_test.go | 50 ++++++++++++++++++++++ cmd/generate-bindings/evm/sourcecre.ts.tpl | 22 +++++++--- 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/cmd/generate-bindings/evm/evm_test.go b/cmd/generate-bindings/evm/evm_test.go index 559b1c49..197442fa 100644 --- a/cmd/generate-bindings/evm/evm_test.go +++ b/cmd/generate-bindings/evm/evm_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "github.com/rs/zerolog" @@ -386,6 +387,55 @@ 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 TestResolveEvmInputs_CustomProjectRoot(t *testing.T) { tempDir, err := os.MkdirTemp("", "generate-bindings-test") require.NoError(t, err) diff --git a/cmd/generate-bindings/evm/sourcecre.ts.tpl b/cmd/generate-bindings/evm/sourcecre.ts.tpl index 13c3f38e..54d19aeb 100644 --- a/cmd/generate-bindings/evm/sourcecre.ts.tpl +++ b/cmd/generate-bindings/evm/sourcecre.ts.tpl @@ -2,11 +2,12 @@ import { decodeEventLog, decodeFunctionResult, + encodeAbiParameters, encodeEventTopics, encodeFunctionData, zeroAddress, } from 'viem' -import type { Address, Hex } from 'viem' +import type { Abi, AbiFunction, Address, Hex } from 'viem' import { bytesToHex, encodeCallMsg, @@ -26,6 +27,14 @@ const encodeTopicValue = (t: Hex | Hex[] | null): string[] => { return [hexToBase64(t)] } +const getFunctionInputs = (abi: Abi, functionName: string): AbiFunction['inputs'] => { + const fn = abi.find( + (item): item is AbiFunction => item.type === 'function' && item.name === functionName, + ) + if (!fn) throw new Error(`function ${functionName} not found in ABI`) + return fn.inputs +} + {{range $contract := .Contracts}} {{/* Event types: Topics (indexed only) and Decoded (all fields) */}} {{range $event := $contract.Events}} @@ -104,14 +113,13 @@ export class {{$contract.Type}} { {{- end}} gasConfig?: { gasLimit?: string }, ) { - const callData = encodeFunctionData({ - abi: {{$contract.Type}}ABI, - functionName: '{{$call.Original.Name}}' as const, - args: [{{range $idx, $param := $call.Normalized.Inputs}}{{if $idx}}, {{end}}{{$param.Name}}{{end}}], - }) + const encodedPayload = encodeAbiParameters( + getFunctionInputs({{$contract.Type}}ABI, '{{$call.Original.Name}}'), + [{{range $idx, $param := $call.Normalized.Inputs}}{{if $idx}}, {{end}}{{$param.Name}}{{end}}], + ) const reportResponse = runtime - .report(prepareReportRequest(callData)) + .report(prepareReportRequest(encodedPayload)) .result() return this.client From b8e27e2be72b04c1bde2706612a6906d988c83ca Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 11 Sep 2026 14:56:43 -0400 Subject: [PATCH 2/7] add golden test --- cmd/generate-bindings/evm/gen_test.go | 211 ++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) diff --git a/cmd/generate-bindings/evm/gen_test.go b/cmd/generate-bindings/evm/gen_test.go index a2fa4688..7a7d0676 100644 --- a/cmd/generate-bindings/evm/gen_test.go +++ b/cmd/generate-bindings/evm/gen_test.go @@ -1,8 +1,14 @@ package evm_test import ( + "os" + "os/exec" + "path/filepath" + "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/cre-cli/cmd/generate-bindings/evm" ) @@ -17,3 +23,208 @@ func TestGenerateBindings(t *testing.T) { t.Fatal(err) } } + +func TestGenerateBindingsCrossLanguageReportPayloadGolden(t *testing.T) { + wd, err := os.Getwd() + require.NoError(t, err) + repoRoot := filepath.Clean(filepath.Join(wd, "../../..")) + + if _, err := exec.LookPath("node"); err != nil { + t.Skip("node is required for TypeScript report payload golden test") + } + if _, err := os.Stat(filepath.Join(repoRoot, "node_modules", "viem")); err != nil { + t.Skip("node_modules/viem is required for TypeScript report payload golden test") + } + + tempDir, err := os.MkdirTemp(wd, "golden-report-payload-") + 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" + } + ]` + abiFile := filepath.Join(tempDir, "PriceUpdater.abi") + require.NoError(t, os.WriteFile(abiFile, []byte(abiContent), 0o600)) + + goOutDir := filepath.Join(tempDir, "priceupdater") + require.NoError(t, os.MkdirAll(goOutDir, 0o755)) + goOutFile := filepath.Join(goOutDir, "price_updater.go") + require.NoError(t, evm.GenerateBindings("", abiFile, "priceupdater", "PriceUpdater", goOutFile)) + + tsOutFile := filepath.Join(tempDir, "PriceUpdater.ts") + require.NoError(t, evm.GenerateBindingsTS(abiFile, "PriceUpdater", tsOutFile)) + + goPayload := generatedGoReportPayloadHex(t, repoRoot, goOutDir) + tsPayload := generatedTSReportPayloadHex(t, repoRoot, tsOutFile) + + require.Equal(t, goPayload, tsPayload) + require.Equal(t, 64, len(goPayload)/2) + require.NotEqual(t, "6adc10b0", goPayload[:8]) +} + +func generatedGoReportPayloadHex(t *testing.T, repoRoot, pkgDir string) string { + t.Helper() + + testFile := filepath.Join(pkgDir, "payload_test.go") + require.NoError(t, os.WriteFile(testFile, []byte(`package priceupdater + +import ( + "fmt" + "math/big" + "testing" +) + +func TestPrintReportPayload(t *testing.T) { + codec, err := NewCodec() + if err != nil { + t.Fatal(err) + } + payload, err := codec.EncodePriceDataStruct(PriceData{ + EthPrice: big.NewInt(123), + BtcPrice: big.NewInt(456), + }) + if err != nil { + t.Fatal(err) + } + fmt.Printf("REPORT_PAYLOAD_HEX=%x\n", payload) +} +`), 0o600)) + + relPkgDir, err := filepath.Rel(repoRoot, pkgDir) + require.NoError(t, err) + cmd := exec.Command("go", "test", "./"+filepath.ToSlash(relPkgDir), "-run", "TestPrintReportPayload", "-v") + cmd.Dir = repoRoot + output, err := cmd.CombinedOutput() + require.NoError(t, err, string(output)) + return extractPayloadHex(t, string(output)) +} + +func generatedTSReportPayloadHex(t *testing.T, repoRoot, tsFile string) string { + t.Helper() + + writeGeneratedTSSDKStub(t, filepath.Dir(tsFile)) + + jsOutDir := filepath.Join(filepath.Dir(tsFile), "js") + cmd := exec.Command( + filepath.Join(repoRoot, "node_modules", ".bin", "tsc"), + tsFile, + "--target", "ES2022", + "--module", "NodeNext", + "--moduleResolution", "NodeNext", + "--outDir", jsOutDir, + "--skipLibCheck", + ) + cmd.Dir = repoRoot + output, err := cmd.CombinedOutput() + require.NoError(t, err, string(output)) + + compiledFile := filepath.Join(jsOutDir, filepath.Base(strings.TrimSuffix(tsFile, ".ts")+".js")) + script := ` +import { pathToFileURL } from 'node:url' + +const { PriceUpdater } = await import(pathToFileURL(process.argv[1]).href) + +let capturedRequest +const runtime = { + report(request) { + capturedRequest = request + return { result: () => ({}) } + }, +} +const client = { + writeReport() { + return { result: () => ({}) } + }, +} + +const binding = new PriceUpdater(client, '0x0000000000000000000000000000000000000001') +binding.writeReportFromUpdatePrices(runtime, { ethPrice: 123n, btcPrice: 456n }) + +if (!capturedRequest?.encodedPayload) throw new Error('report request payload not captured') +console.log('REPORT_PAYLOAD_HEX=' + Buffer.from(capturedRequest.encodedPayload, 'base64').toString('hex')) +` + cmd = exec.Command("node", "--input-type=module", "-e", script, compiledFile) + cmd.Dir = repoRoot + output, err = cmd.CombinedOutput() + require.NoError(t, err, string(output)) + return extractPayloadHex(t, string(output)) +} + +func writeGeneratedTSSDKStub(t *testing.T, dir string) { + t.Helper() + + require.NoError(t, os.WriteFile(filepath.Join(dir, "package.json"), []byte(`{"type":"module"}`), 0o600)) + + sdkDir := filepath.Join(dir, "node_modules", "@chainlink", "cre-sdk") + require.NoError(t, os.MkdirAll(sdkDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(sdkDir, "package.json"), []byte(`{ + "type": "module", + "exports": { + ".": { + "types": "./index.d.ts", + "import": "./index.js" + } + } +}`), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(sdkDir, "index.js"), []byte(` +export const zeroAddress = '0x0000000000000000000000000000000000000000' +export const LAST_FINALIZED_BLOCK_NUMBER = {} +export class EVMClient {} +export const bytesToHex = (bytes) => '0x' + Buffer.from(bytes).toString('hex') +export const hexToBase64 = (hex) => Buffer.from(hex.slice(2), 'hex').toString('base64') +export const encodeCallMsg = (call) => call +export const prepareReportRequest = (hexPayload) => ({ + encodedPayload: Buffer.from(hexPayload.slice(2), 'hex').toString('base64'), + encoderName: 'evm', + signingAlgo: 'ecdsa', + hashingAlgo: 'keccak256', +}) +`), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(sdkDir, "index.d.ts"), []byte(` +export type Runtime = { + report(request: unknown): { result(): unknown } +} +export type EVMLog = { data: Uint8Array; topics: Uint8Array[] } +export declare const zeroAddress: '0x0000000000000000000000000000000000000000' +export declare const LAST_FINALIZED_BLOCK_NUMBER: unknown +export declare class EVMClient { + callContract(runtime: unknown, input: unknown): { result(): { data: Uint8Array } } + writeReport(runtime: unknown, input: unknown): { result(): unknown } +} +export declare const bytesToHex: (bytes: Uint8Array) => `+"`0x${string}`"+` +export declare const hexToBase64: (hex: `+"`0x${string}`"+`) => string +export declare const encodeCallMsg: (call: T) => T +export declare const prepareReportRequest: (hexPayload: `+"`0x${string}`"+`) => { + encodedPayload: string + encoderName: string + signingAlgo: string + hashingAlgo: string +} +`), 0o600)) +} + +func extractPayloadHex(t *testing.T, output string) string { + t.Helper() + + for _, line := range strings.Split(output, "\n") { + if payload, ok := strings.CutPrefix(strings.TrimSpace(line), "REPORT_PAYLOAD_HEX="); ok { + return payload + } + } + t.Fatalf("REPORT_PAYLOAD_HEX not found in output:\n%s", output) + return "" +} From 8c1bd2e5e22e9862bb25686243afaacf81c09348 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 11 Sep 2026 15:43:44 -0400 Subject: [PATCH 3/7] cleanup --- cmd/generate-bindings/evm/gen_test.go | 56 ++++++++++----------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/cmd/generate-bindings/evm/gen_test.go b/cmd/generate-bindings/evm/gen_test.go index 7a7d0676..2c796fc4 100644 --- a/cmd/generate-bindings/evm/gen_test.go +++ b/cmd/generate-bindings/evm/gen_test.go @@ -35,6 +35,9 @@ func TestGenerateBindingsCrossLanguageReportPayloadGolden(t *testing.T) { if _, err := os.Stat(filepath.Join(repoRoot, "node_modules", "viem")); err != nil { t.Skip("node_modules/viem is required for TypeScript report payload golden test") } + if _, err := os.Stat(filepath.Join(repoRoot, "node_modules", "typescript")); err != nil { + t.Skip("node_modules/typescript is required for TypeScript report payload golden test") + } tempDir, err := os.MkdirTemp(wd, "golden-report-payload-") require.NoError(t, err) @@ -119,20 +122,27 @@ func generatedTSReportPayloadHex(t *testing.T, repoRoot, tsFile string) string { writeGeneratedTSSDKStub(t, filepath.Dir(tsFile)) jsOutDir := filepath.Join(filepath.Dir(tsFile), "js") - cmd := exec.Command( - filepath.Join(repoRoot, "node_modules", ".bin", "tsc"), - tsFile, - "--target", "ES2022", - "--module", "NodeNext", - "--moduleResolution", "NodeNext", - "--outDir", jsOutDir, - "--skipLibCheck", - ) + require.NoError(t, os.MkdirAll(jsOutDir, 0o755)) + + compiledFile := filepath.Join(jsOutDir, filepath.Base(strings.TrimSuffix(tsFile, ".ts")+".js")) + transpileScript := ` +import { readFileSync, writeFileSync } from 'node:fs' +import ts from 'typescript' + +const src = readFileSync(process.argv[1], 'utf8') +const result = ts.transpileModule(src, { + compilerOptions: { + module: ts.ModuleKind.ES2022, + target: ts.ScriptTarget.ES2022, + }, +}) +writeFileSync(process.argv[2], result.outputText) +` + cmd := exec.Command("node", "--input-type=module", "-e", transpileScript, tsFile, compiledFile) cmd.Dir = repoRoot output, err := cmd.CombinedOutput() require.NoError(t, err, string(output)) - compiledFile := filepath.Join(jsOutDir, filepath.Base(strings.TrimSuffix(tsFile, ".ts")+".js")) script := ` import { pathToFileURL } from 'node:url' @@ -181,39 +191,13 @@ func writeGeneratedTSSDKStub(t *testing.T, dir string) { } }`), 0o600)) require.NoError(t, os.WriteFile(filepath.Join(sdkDir, "index.js"), []byte(` -export const zeroAddress = '0x0000000000000000000000000000000000000000' -export const LAST_FINALIZED_BLOCK_NUMBER = {} -export class EVMClient {} -export const bytesToHex = (bytes) => '0x' + Buffer.from(bytes).toString('hex') export const hexToBase64 = (hex) => Buffer.from(hex.slice(2), 'hex').toString('base64') -export const encodeCallMsg = (call) => call export const prepareReportRequest = (hexPayload) => ({ encodedPayload: Buffer.from(hexPayload.slice(2), 'hex').toString('base64'), encoderName: 'evm', signingAlgo: 'ecdsa', hashingAlgo: 'keccak256', }) -`), 0o600)) - require.NoError(t, os.WriteFile(filepath.Join(sdkDir, "index.d.ts"), []byte(` -export type Runtime = { - report(request: unknown): { result(): unknown } -} -export type EVMLog = { data: Uint8Array; topics: Uint8Array[] } -export declare const zeroAddress: '0x0000000000000000000000000000000000000000' -export declare const LAST_FINALIZED_BLOCK_NUMBER: unknown -export declare class EVMClient { - callContract(runtime: unknown, input: unknown): { result(): { data: Uint8Array } } - writeReport(runtime: unknown, input: unknown): { result(): unknown } -} -export declare const bytesToHex: (bytes: Uint8Array) => `+"`0x${string}`"+` -export declare const hexToBase64: (hex: `+"`0x${string}`"+`) => string -export declare const encodeCallMsg: (call: T) => T -export declare const prepareReportRequest: (hexPayload: `+"`0x${string}`"+`) => { - encodedPayload: string - encoderName: string - signingAlgo: string - hashingAlgo: string -} `), 0o600)) } From 512bc28392b9fbfa6f6ca3fa9f2c2f826688422e Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 11 Sep 2026 15:59:48 -0400 Subject: [PATCH 4/7] fix lint --- cmd/generate-bindings/evm/gen_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/generate-bindings/evm/gen_test.go b/cmd/generate-bindings/evm/gen_test.go index 2c796fc4..2a5f4a61 100644 --- a/cmd/generate-bindings/evm/gen_test.go +++ b/cmd/generate-bindings/evm/gen_test.go @@ -109,7 +109,7 @@ func TestPrintReportPayload(t *testing.T) { relPkgDir, err := filepath.Rel(repoRoot, pkgDir) require.NoError(t, err) - cmd := exec.Command("go", "test", "./"+filepath.ToSlash(relPkgDir), "-run", "TestPrintReportPayload", "-v") + cmd := exec.Command("go", "test", "./"+filepath.ToSlash(relPkgDir), "-run", "TestPrintReportPayload", "-v") //nolint:gosec // G204 -- relPkgDir is a test-created temp package under this repo. cmd.Dir = repoRoot output, err := cmd.CombinedOutput() require.NoError(t, err, string(output)) From 291879c3d3028629a994c99203b37c17088dcc8e Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 8 Oct 2026 11:46:00 -0400 Subject: [PATCH 5/7] include legacy selector flag --- cmd/generate-bindings/evm/abigen/bindv2.go | 17 ++++++- cmd/generate-bindings/evm/bindgen.go | 8 +++- cmd/generate-bindings/evm/evm.go | 39 ++++++++++------ cmd/generate-bindings/evm/evm_test.go | 54 ++++++++++++++++++++++ cmd/generate-bindings/evm/gen_test.go | 9 ++++ cmd/generate-bindings/evm/sourcecre.ts.tpl | 14 +++++- docs/cre_generate-bindings_evm.md | 16 ++++--- 7 files changed, 132 insertions(+), 25 deletions(-) diff --git a/cmd/generate-bindings/evm/abigen/bindv2.go b/cmd/generate-bindings/evm/abigen/bindv2.go index d1781fe9..feb8d04b 100644 --- a/cmd/generate-bindings/evm/abigen/bindv2.go +++ b/cmd/generate-bindings/evm/abigen/bindv2.go @@ -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), @@ -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 diff --git a/cmd/generate-bindings/evm/bindgen.go b/cmd/generate-bindings/evm/bindgen.go index f79e3717..dd000248 100644 --- a/cmd/generate-bindings/evm/bindgen.go +++ b/cmd/generate-bindings/evm/bindgen.go @@ -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") @@ -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) } diff --git a/cmd/generate-bindings/evm/evm.go b/cmd/generate-bindings/evm/evm.go index e6abb35f..dbc4c3fa 100644 --- a/cmd/generate-bindings/evm/evm.go +++ b/cmd/generate-bindings/evm/evm.go @@ -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 { @@ -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) @@ -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 } @@ -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 } @@ -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) @@ -363,6 +371,7 @@ func (h *handler) processSingleAbi(inputs Inputs) error { inputs.AbiPath, contractName, outputFile, + TSBindingOptions{IncludeFunctionSelector: inputs.IncludeFunctionSelector}, ); err != nil { return err } diff --git a/cmd/generate-bindings/evm/evm_test.go b/cmd/generate-bindings/evm/evm_test.go index 197442fa..d8b939cb 100644 --- a/cmd/generate-bindings/evm/evm_test.go +++ b/cmd/generate-bindings/evm/evm_test.go @@ -436,6 +436,60 @@ func TestGenerateBindingsTS_WriteReportHelperEncodesArgsOnly(t *testing.T) { 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) diff --git a/cmd/generate-bindings/evm/gen_test.go b/cmd/generate-bindings/evm/gen_test.go index 2a5f4a61..7768d8a0 100644 --- a/cmd/generate-bindings/evm/gen_test.go +++ b/cmd/generate-bindings/evm/gen_test.go @@ -77,6 +77,15 @@ func TestGenerateBindingsCrossLanguageReportPayloadGolden(t *testing.T) { require.Equal(t, goPayload, tsPayload) require.Equal(t, 64, len(goPayload)/2) require.NotEqual(t, "6adc10b0", goPayload[:8]) + + legacyDir := filepath.Join(tempDir, "legacy") + require.NoError(t, os.MkdirAll(legacyDir, 0o755)) + legacyFile := filepath.Join(legacyDir, "PriceUpdater.ts") + require.NoError(t, evm.GenerateBindingsTS(abiFile, "PriceUpdater", legacyFile, + evm.TSBindingOptions{IncludeFunctionSelector: true})) + legacyPayload := generatedTSReportPayloadHex(t, repoRoot, legacyFile) + require.Equal(t, "6adc10b0"+goPayload, legacyPayload, "legacy reports must contain the selector followed by the same encoded arguments") + require.Equal(t, 68, len(legacyPayload)/2) } func generatedGoReportPayloadHex(t *testing.T, repoRoot, pkgDir string) string { diff --git a/cmd/generate-bindings/evm/sourcecre.ts.tpl b/cmd/generate-bindings/evm/sourcecre.ts.tpl index 54d19aeb..7dacd417 100644 --- a/cmd/generate-bindings/evm/sourcecre.ts.tpl +++ b/cmd/generate-bindings/evm/sourcecre.ts.tpl @@ -2,12 +2,14 @@ import { decodeEventLog, decodeFunctionResult, + {{- if not .IncludeFunctionSelector}} encodeAbiParameters, + {{- end}} encodeEventTopics, encodeFunctionData, zeroAddress, } from 'viem' -import type { Abi, AbiFunction, Address, Hex } from 'viem' +import type { {{if not .IncludeFunctionSelector}}Abi, AbiFunction, {{end}}Address, Hex } from 'viem' import { bytesToHex, encodeCallMsg, @@ -27,6 +29,7 @@ const encodeTopicValue = (t: Hex | Hex[] | null): string[] => { return [hexToBase64(t)] } +{{if not .IncludeFunctionSelector}} const getFunctionInputs = (abi: Abi, functionName: string): AbiFunction['inputs'] => { const fn = abi.find( (item): item is AbiFunction => item.type === 'function' && item.name === functionName, @@ -34,6 +37,7 @@ const getFunctionInputs = (abi: Abi, functionName: string): AbiFunction['inputs' if (!fn) throw new Error(`function ${functionName} not found in ABI`) return fn.inputs } +{{end}} {{range $contract := .Contracts}} {{/* Event types: Topics (indexed only) and Decoded (all fields) */}} @@ -113,10 +117,18 @@ export class {{$contract.Type}} { {{- end}} gasConfig?: { gasLimit?: string }, ) { + {{- if $.IncludeFunctionSelector}} + const encodedPayload = encodeFunctionData({ + abi: {{$contract.Type}}ABI, + functionName: '{{$call.Original.Name}}' as const, + args: [{{range $idx, $param := $call.Normalized.Inputs}}{{if $idx}}, {{end}}{{$param.Name}}{{end}}], + }) + {{- else}} const encodedPayload = encodeAbiParameters( getFunctionInputs({{$contract.Type}}ABI, '{{$call.Original.Name}}'), [{{range $idx, $param := $call.Normalized.Inputs}}{{if $idx}}, {{end}}{{$param.Name}}{{end}}], ) + {{- end}} const reportResponse = runtime .report(prepareReportRequest(encodedPayload)) diff --git a/docs/cre_generate-bindings_evm.md b/docs/cre_generate-bindings_evm.md index 6685ff00..b37e65a0 100644 --- a/docs/cre_generate-bindings_evm.md +++ b/docs/cre_generate-bindings_evm.md @@ -14,6 +14,10 @@ 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. +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. + ``` cre generate-bindings evm [optional flags] ``` @@ -27,11 +31,12 @@ cre generate-bindings evm [optional flags] ### Options ``` - -a, --abi string Path to ABI directory (defaults to contracts/evm/src/abi/). Supports *.abi and *.json files - -h, --help help for evm - -l, --language string Target language: go, typescript (auto-detected from project files when omitted) - -k, --pkg string Base package name (each contract gets its own subdirectory) (default "bindings") - -p, --project-root string Path to project root directory (defaults to current directory) + -a, --abi string Path to ABI directory (defaults to contracts/evm/src/abi/). Supports *.abi and *.json files + -h, --help help for evm + --include-function-selector Include the function selector in TypeScript report payloads (legacy behavior) + -l, --language string Target language: go, typescript (auto-detected from project files when omitted) + -k, --pkg string Base package name (each contract gets its own subdirectory) (default "bindings") + -p, --project-root string Path to project root directory (defaults to current directory) ``` ### Options inherited from parent commands @@ -49,4 +54,3 @@ cre generate-bindings evm [optional flags] ### SEE ALSO * [cre generate-bindings](cre_generate-bindings.md) - Generate bindings for contracts - From 752398ae7c8727028aa7ff28ef07bd3b5be2e70d Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 8 Oct 2026 11:53:31 -0400 Subject: [PATCH 6/7] add changeset --- .changeset/evm-bindings-report-payload.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/evm-bindings-report-payload.md diff --git a/.changeset/evm-bindings-report-payload.md b/.changeset/evm-bindings-report-payload.md new file mode 100644 index 00000000..e3302768 --- /dev/null +++ b/.changeset/evm-bindings-report-payload.md @@ -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. From dea9a2cb271d878aad8cba43addc3ef3fe98e98a Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 8 Oct 2026 13:56:27 -0400 Subject: [PATCH 7/7] gendoc --- docs/cre_generate-bindings_evm.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/cre_generate-bindings_evm.md b/docs/cre_generate-bindings_evm.md index b37e65a0..3cb81180 100644 --- a/docs/cre_generate-bindings_evm.md +++ b/docs/cre_generate-bindings_evm.md @@ -54,3 +54,4 @@ cre generate-bindings evm [optional flags] ### SEE ALSO * [cre generate-bindings](cre_generate-bindings.md) - Generate bindings for contracts +