From 6ffab58727fa019ec4957b378392a16759aff7c0 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 11 Sep 2026 14:26:42 -0400 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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))