diff --git a/cmd/creinit/creinit.go b/cmd/creinit/creinit.go index df078e72..b1d9caa7 100644 --- a/cmd/creinit/creinit.go +++ b/cmd/creinit/creinit.go @@ -302,6 +302,19 @@ func (h *handler) Execute(inputs Inputs) error { } } + // Install contracts dependencies for TypeScript projects with contracts + if selectedLanguageTemplate.Lang == TemplateLangTS && contractsGenerated { + contractsDir := filepath.Join(projectRoot, "contracts") + contractsPkg := filepath.Join(contractsDir, "package.json") + if h.pathExists(contractsPkg) { + spinner.Update("Installing contracts dependencies...") + if err := runBunInstall(h.log, contractsDir); err != nil { + spinner.Stop() + return fmt.Errorf("failed to install contracts dependencies: %w", err) + } + } + } + // Generate workflow settings spinner.Update("Generating workflow settings...") _, err = settings.GenerateWorkflowSettingsFile(workflowDirectory, workflowName, selectedLanguageTemplate.EntryPoint) @@ -487,6 +500,11 @@ func (h *handler) generateWorkflowTemplate(workingDirectory string, template Wor return nil } + // Skip generated directory - TS bindings live in contracts/evm/src/generated/ + if strings.HasPrefix(relPath, "generated") { + return nil + } + // If it's a directory, just create the matching directory in the working dir if d.IsDir() { return os.MkdirAll(filepath.Join(workingDirectory, relPath), 0o755) diff --git a/cmd/creinit/creinit_test.go b/cmd/creinit/creinit_test.go index a71cedaa..5da7e83e 100644 --- a/cmd/creinit/creinit_test.go +++ b/cmd/creinit/creinit_test.go @@ -1,14 +1,19 @@ package creinit import ( + "crypto/sha256" + "encoding/hex" "fmt" + "io/fs" "os" "os/exec" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/cre-cli/cmd/generate-bindings/bindings" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/testutil" "github.com/smartcontractkit/cre-cli/internal/testutil/chainsim" @@ -78,6 +83,109 @@ func requireNoDirExists(t *testing.T, dirPath string) { require.Falsef(t, fi.IsDir(), "directory %s should NOT exist", dirPath) } +func hashDirectoryFiles(t *testing.T, dir string) map[string]string { + t.Helper() + hashes := make(map[string]string) + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + rel, _ := filepath.Rel(dir, path) + sum := sha256.Sum256(data) + hashes[rel] = hex.EncodeToString(sum[:]) + return nil + }) + require.NoError(t, err) + return hashes +} + +func validateGeneratedBindingsStable(t *testing.T, projectRoot, workflowName, language string) { + t.Helper() + + abiDir := filepath.Join(projectRoot, "contracts", "evm", "src", "abi") + + var generatedDir string + switch language { + case "go": + generatedDir = filepath.Join(projectRoot, "contracts", "evm", "src", "generated") + case "typescript": + generatedDir = filepath.Join(projectRoot, "contracts", "evm", "ts", "generated") + default: + return + } + + if _, err := os.Stat(generatedDir); os.IsNotExist(err) { + return + } + + beforeHashes := hashDirectoryFiles(t, generatedDir) + require.NotEmpty(t, beforeHashes, "generated directory should not be empty") + + abiFiles, err := filepath.Glob(filepath.Join(abiDir, "*.abi")) + require.NoError(t, err) + require.NotEmpty(t, abiFiles, "abi directory should contain *.abi files") + + switch language { + case "go": + for _, abiFile := range abiFiles { + contractName := strings.TrimSuffix(filepath.Base(abiFile), ".abi") + entries, readErr := os.ReadDir(generatedDir) + require.NoError(t, readErr) + + found := false + for _, entry := range entries { + if !entry.IsDir() { + continue + } + goFile := filepath.Join(generatedDir, entry.Name(), contractName+".go") + if _, statErr := os.Stat(goFile); statErr == nil { + err = bindings.GenerateBindings("", abiFile, entry.Name(), contractName, goFile) + require.NoError(t, err, "failed to regenerate Go bindings for %s", contractName) + found = true + break + } + } + require.True(t, found, "no matching generated directory found for contract %s", contractName) + } + + case "typescript": + var generatedContracts []string + for _, abiFile := range abiFiles { + ext := filepath.Ext(abiFile) + contractName := strings.TrimSuffix(filepath.Base(abiFile), ext) + outFile := filepath.Join(generatedDir, contractName+".ts") + err = bindings.GenerateBindingsTS(abiFile, contractName, outFile) + require.NoError(t, err, "failed to regenerate TS bindings for %s", contractName) + generatedContracts = append(generatedContracts, contractName) + } + // Regenerate barrel index.ts + var indexContent string + indexContent += "// Code generated — DO NOT EDIT.\n" + for _, name := range generatedContracts { + indexContent += fmt.Sprintf("export * from './%s'\n", name) + indexContent += fmt.Sprintf("export * from './%s_mock'\n", name) + } + indexPath := filepath.Join(generatedDir, "index.ts") + require.NoError(t, os.WriteFile(indexPath, []byte(indexContent), 0o600)) + } + + afterHashes := hashDirectoryFiles(t, generatedDir) + + require.Equal(t, len(beforeHashes), len(afterHashes), "number of generated files changed after regeneration") + for file, beforeHash := range beforeHashes { + afterHash, exists := afterHashes[file] + require.True(t, exists, "generated file %s disappeared after regeneration", file) + require.Equal(t, beforeHash, afterHash, "generated file %s changed after regeneration — template is stale", file) + } +} + // runLanguageSpecificTests runs the appropriate test suite based on the language field. // For TypeScript: runs bun install and bun test in the workflow directory. // For Go: runs go test ./... in the workflow directory. @@ -96,6 +204,8 @@ func runLanguageSpecificTests(t *testing.T, workflowDir, language string) { // runTypescriptTests executes TypeScript tests using bun. // Follows the cre init instructions: bun install --cwd then bun test in that directory. +// For projects with contracts (e.g. TS PoR), also installs contracts dependencies so generated +// bindings can resolve @chainlink/cre-sdk. func runTypescriptTests(t *testing.T, workflowDir string) { t.Helper() @@ -105,7 +215,17 @@ func runTypescriptTests(t *testing.T, workflowDir string) { require.NoError(t, err, "bun install failed in %s:\n%s", workflowDir, string(installOutput)) t.Logf("bun install succeeded") - // Run tests + // Install contracts dependencies when contracts/package.json exists (TS PoR template) + projectRoot := filepath.Dir(workflowDir) + contractsPkg := filepath.Join(projectRoot, "contracts", "package.json") + if _, err := os.Stat(contractsPkg); err == nil { + contractsDir := filepath.Join(projectRoot, "contracts") + installCmd := exec.Command("bun", "install", "--cwd", contractsDir, "--ignore-scripts") + installOutput, err := installCmd.CombinedOutput() + require.NoError(t, err, "bun install failed in %s:\n%s", contractsDir, string(installOutput)) + t.Logf("bun install in contracts succeeded") + } + testCmd := exec.Command("bun", "test") testCmd.Dir = workflowDir testOutput, err := testCmd.CombinedOutput() @@ -256,6 +376,7 @@ func TestInitExecuteFlows(t *testing.T) { validateInitProjectStructure(t, projectRoot, tc.expectWorkflowName, tc.expectTemplateFiles) runLanguageSpecificTests(t, filepath.Join(projectRoot, tc.expectWorkflowName), tc.language) + validateGeneratedBindingsStable(t, projectRoot, tc.expectWorkflowName, tc.language) }) } } diff --git a/cmd/creinit/go_module_init.go b/cmd/creinit/go_module_init.go index 759131c8..419c3e6b 100644 --- a/cmd/creinit/go_module_init.go +++ b/cmd/creinit/go_module_init.go @@ -82,3 +82,7 @@ func runCommand(logger *zerolog.Logger, dir, command string, args ...string) err logger.Debug().Msgf("Command succeeded: %s %v", command, args) return nil } + +func runBunInstall(logger *zerolog.Logger, dir string) error { + return runCommand(logger, dir, "bun", "install", "--ignore-scripts") +} diff --git a/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/ierc20/IERC20.go b/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/ierc20/IERC20.go index 1a57677d..00adf268 100644 --- a/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/ierc20/IERC20.go +++ b/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/ierc20/IERC20.go @@ -355,23 +355,7 @@ func (c *Codec) EncodeApprovalTopics( return nil, err } - topics := make([]*evm.TopicValues, len(rawTopics)+1) - topics[0] = &evm.TopicValues{ - Values: [][]byte{evt.ID.Bytes()}, - } - for i, hashList := range rawTopics { - bs := make([][]byte, len(hashList)) - for j, h := range hashList { - // don't include empty bytes if hashed value is 0x0 - if reflect.ValueOf(h).IsZero() { - bs[j] = []byte{} - } else { - bs[j] = h.Bytes() - } - } - topics[i+1] = &evm.TopicValues{Values: bs} - } - return topics, nil + return bindings.PrepareTopics(rawTopics, evt.ID.Bytes()), nil } // DecodeApproval decodes a log into a Approval struct. @@ -444,23 +428,7 @@ func (c *Codec) EncodeTransferTopics( return nil, err } - topics := make([]*evm.TopicValues, len(rawTopics)+1) - topics[0] = &evm.TopicValues{ - Values: [][]byte{evt.ID.Bytes()}, - } - for i, hashList := range rawTopics { - bs := make([][]byte, len(hashList)) - for j, h := range hashList { - // don't include empty bytes if hashed value is 0x0 - if reflect.ValueOf(h).IsZero() { - bs[j] = []byte{} - } else { - bs[j] = h.Bytes() - } - } - topics[i+1] = &evm.TopicValues{Values: bs} - } - return topics, nil + return bindings.PrepareTopics(rawTopics, evt.ID.Bytes()), nil } // DecodeTransfer decodes a log into a Transfer struct. @@ -663,11 +631,9 @@ func (c *IERC20) LogTriggerApprovalLog(chainSelector uint64, confidence evm.Conf }, nil } -func (c *IERC20) FilterLogsApproval(runtime cre.Runtime, options *bindings.FilterOptions) cre.Promise[*evm.FilterLogsReply] { +func (c *IERC20) FilterLogsApproval(runtime cre.Runtime, options *bindings.FilterOptions) (cre.Promise[*evm.FilterLogsReply], error) { if options == nil { - options = &bindings.FilterOptions{ - ToBlock: options.ToBlock, - } + return nil, errors.New("FilterLogs options are required.") } return c.client.FilterLogs(runtime, &evm.FilterLogsRequest{ FilterQuery: &evm.FilterQuery{ @@ -679,7 +645,7 @@ func (c *IERC20) FilterLogsApproval(runtime cre.Runtime, options *bindings.Filte FromBlock: pb.NewBigIntFromInt(options.FromBlock), ToBlock: pb.NewBigIntFromInt(options.ToBlock), }, - }) + }), nil } // TransferTrigger wraps the raw log trigger and provides decoded TransferDecoded data @@ -721,11 +687,9 @@ func (c *IERC20) LogTriggerTransferLog(chainSelector uint64, confidence evm.Conf }, nil } -func (c *IERC20) FilterLogsTransfer(runtime cre.Runtime, options *bindings.FilterOptions) cre.Promise[*evm.FilterLogsReply] { +func (c *IERC20) FilterLogsTransfer(runtime cre.Runtime, options *bindings.FilterOptions) (cre.Promise[*evm.FilterLogsReply], error) { if options == nil { - options = &bindings.FilterOptions{ - ToBlock: options.ToBlock, - } + return nil, errors.New("FilterLogs options are required.") } return c.client.FilterLogs(runtime, &evm.FilterLogsRequest{ FilterQuery: &evm.FilterQuery{ @@ -737,5 +701,5 @@ func (c *IERC20) FilterLogsTransfer(runtime cre.Runtime, options *bindings.Filte FromBlock: pb.NewBigIntFromInt(options.FromBlock), ToBlock: pb.NewBigIntFromInt(options.ToBlock), }, - }) + }), nil } diff --git a/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/reserve_manager/ReserveManager.go b/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/reserve_manager/ReserveManager.go index 89a5b9ab..d089e61b 100644 --- a/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/reserve_manager/ReserveManager.go +++ b/cmd/creinit/template/workflow/porExampleDev/contracts/evm/src/generated/reserve_manager/ReserveManager.go @@ -250,23 +250,7 @@ func (c *Codec) EncodeRequestReserveUpdateTopics( return nil, err } - topics := make([]*evm.TopicValues, len(rawTopics)+1) - topics[0] = &evm.TopicValues{ - Values: [][]byte{evt.ID.Bytes()}, - } - for i, hashList := range rawTopics { - bs := make([][]byte, len(hashList)) - for j, h := range hashList { - // don't include empty bytes if hashed value is 0x0 - if reflect.ValueOf(h).IsZero() { - bs[j] = []byte{} - } else { - bs[j] = h.Bytes() - } - } - topics[i+1] = &evm.TopicValues{Values: bs} - } - return topics, nil + return bindings.PrepareTopics(rawTopics, evt.ID.Bytes()), nil } // DecodeRequestReserveUpdate decodes a log into a RequestReserveUpdate struct. @@ -455,11 +439,9 @@ func (c *ReserveManager) LogTriggerRequestReserveUpdateLog(chainSelector uint64, }, nil } -func (c *ReserveManager) FilterLogsRequestReserveUpdate(runtime cre.Runtime, options *bindings.FilterOptions) cre.Promise[*evm.FilterLogsReply] { +func (c *ReserveManager) FilterLogsRequestReserveUpdate(runtime cre.Runtime, options *bindings.FilterOptions) (cre.Promise[*evm.FilterLogsReply], error) { if options == nil { - options = &bindings.FilterOptions{ - ToBlock: options.ToBlock, - } + return nil, errors.New("FilterLogs options are required.") } return c.client.FilterLogs(runtime, &evm.FilterLogsRequest{ FilterQuery: &evm.FilterQuery{ @@ -471,5 +453,5 @@ func (c *ReserveManager) FilterLogsRequestReserveUpdate(runtime cre.Runtime, opt FromBlock: pb.NewBigIntFromInt(options.FromBlock), ToBlock: pb.NewBigIntFromInt(options.ToBlock), }, - }) + }), nil } diff --git a/cmd/creinit/template/workflow/typescriptConfHTTP/package.json.tpl b/cmd/creinit/template/workflow/typescriptConfHTTP/package.json.tpl index e0af3745..1d5a11a4 100644 --- a/cmd/creinit/template/workflow/typescriptConfHTTP/package.json.tpl +++ b/cmd/creinit/template/workflow/typescriptConfHTTP/package.json.tpl @@ -8,7 +8,7 @@ }, "license": "UNLICENSED", "dependencies": { - "@chainlink/cre-sdk": "^1.1.1", + "@chainlink/cre-sdk": "^1.1.2", "zod": "3.25.76" }, "devDependencies": { diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/BalanceReader.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/BalanceReader.ts.tpl deleted file mode 100644 index 2cb90454..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/BalanceReader.ts.tpl +++ /dev/null @@ -1,16 +0,0 @@ -export const BalanceReader = [ - { - inputs: [{ internalType: 'address[]', name: 'addresses', type: 'address[]' }], - name: 'getNativeBalances', - outputs: [{ internalType: 'uint256[]', name: '', type: 'uint256[]' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'typeAndVersion', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC165.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC165.ts.tpl deleted file mode 100644 index d41a3f22..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC165.ts.tpl +++ /dev/null @@ -1,9 +0,0 @@ -export const IERC165 = [ - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC20.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC20.ts.tpl deleted file mode 100644 index a2e017e5..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IERC20.ts.tpl +++ /dev/null @@ -1,97 +0,0 @@ -export const IERC20 = [ - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Approval', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'from', type: 'address' }, - { indexed: true, internalType: 'address', name: 'to', type: 'address' }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Transfer', - type: 'event', - }, - { - inputs: [ - { internalType: 'address', name: 'owner', type: 'address' }, - { internalType: 'address', name: 'spender', type: 'address' }, - ], - name: 'allowance', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'account', type: 'address' }], - name: 'balanceOf', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'totalSupply', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'recipient', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'transfer', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'sender', type: 'address' }, - { internalType: 'address', name: 'recipient', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'transferFrom', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiver.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiver.ts.tpl deleted file mode 100644 index a10cfc0a..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiver.ts.tpl +++ /dev/null @@ -1,19 +0,0 @@ -export const IReceiver = [ - { - inputs: [ - { internalType: 'bytes', name: 'metadata', type: 'bytes' }, - { internalType: 'bytes', name: 'report', type: 'bytes' }, - ], - name: 'onReport', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'view', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiverTemplate.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiverTemplate.ts.tpl deleted file mode 100644 index bb230ef7..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReceiverTemplate.ts.tpl +++ /dev/null @@ -1,49 +0,0 @@ -export const IReceiverTemplate = [ - { - inputs: [ - { internalType: 'address', name: 'received', type: 'address' }, - { internalType: 'address', name: 'expected', type: 'address' }, - ], - name: 'InvalidAuthor', - type: 'error', - }, - { - inputs: [ - { internalType: 'bytes10', name: 'received', type: 'bytes10' }, - { internalType: 'bytes10', name: 'expected', type: 'bytes10' }, - ], - name: 'InvalidWorkflowName', - type: 'error', - }, - { - inputs: [], - name: 'EXPECTED_AUTHOR', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'EXPECTED_WORKFLOW_NAME', - outputs: [{ internalType: 'bytes10', name: '', type: 'bytes10' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes', name: 'metadata', type: 'bytes' }, - { internalType: 'bytes', name: 'report', type: 'bytes' }, - ], - name: 'onReport', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'pure', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReserveManager.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReserveManager.ts.tpl deleted file mode 100644 index b19aa351..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/IReserveManager.ts.tpl +++ /dev/null @@ -1,32 +0,0 @@ -export const IReserveManager = [ - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'requestId', - type: 'uint256', - }, - ], - name: 'RequestReserveUpdate', - type: 'event', - }, - { - inputs: [ - { - components: [ - { internalType: 'uint256', name: 'totalMinted', type: 'uint256' }, - { internalType: 'uint256', name: 'totalReserve', type: 'uint256' }, - ], - internalType: 'struct UpdateReserves', - name: 'updateReserves', - type: 'tuple', - }, - ], - name: 'updateReserves', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ITypeAndVersion.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ITypeAndVersion.ts.tpl deleted file mode 100644 index 84298663..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ITypeAndVersion.ts.tpl +++ /dev/null @@ -1,9 +0,0 @@ -export const ITypeAndVersion = [ - { - inputs: [], - name: 'typeAndVersion', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'pure', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/MessageEmitter.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/MessageEmitter.ts.tpl deleted file mode 100644 index 5f3a2b08..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/MessageEmitter.ts.tpl +++ /dev/null @@ -1,58 +0,0 @@ -export const MessageEmitter = [ - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'emitter', - type: 'address', - }, - { - indexed: true, - internalType: 'uint256', - name: 'timestamp', - type: 'uint256', - }, - { - indexed: false, - internalType: 'string', - name: 'message', - type: 'string', - }, - ], - name: 'MessageEmitted', - type: 'event', - }, - { - inputs: [{ internalType: 'string', name: 'message', type: 'string' }], - name: 'emitMessage', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'emitter', type: 'address' }], - name: 'getLastMessage', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'emitter', type: 'address' }, - { internalType: 'uint256', name: 'timestamp', type: 'uint256' }, - ], - name: 'getMessage', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'typeAndVersion', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ReserveManager.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ReserveManager.ts.tpl deleted file mode 100644 index 611e4129..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/ReserveManager.ts.tpl +++ /dev/null @@ -1,46 +0,0 @@ -export const ReserveManager = [ - { - anonymous: false, - inputs: [ - { - indexed: false, - internalType: 'uint256', - name: 'requestId', - type: 'uint256', - }, - ], - name: 'RequestReserveUpdate', - type: 'event', - }, - { - inputs: [], - name: 'lastTotalMinted', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'lastTotalReserve', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { - components: [ - { internalType: 'uint256', name: 'totalMinted', type: 'uint256' }, - { internalType: 'uint256', name: 'totalReserve', type: 'uint256' }, - ], - internalType: 'struct UpdateReserves', - name: 'updateReserves', - type: 'tuple', - }, - ], - name: 'updateReserves', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/SimpleERC20.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/SimpleERC20.ts.tpl deleted file mode 100644 index 31ec3d30..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/SimpleERC20.ts.tpl +++ /dev/null @@ -1,127 +0,0 @@ -export const SimpleERC20 = [ - { - inputs: [ - { internalType: 'string', name: '_name', type: 'string' }, - { internalType: 'string', name: '_symbol', type: 'string' }, - { internalType: 'uint256', name: '_initialSupply', type: 'uint256' }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - anonymous: false, - inputs: [ - { - indexed: true, - internalType: 'address', - name: 'owner', - type: 'address', - }, - { - indexed: true, - internalType: 'address', - name: 'spender', - type: 'address', - }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Approval', - type: 'event', - }, - { - anonymous: false, - inputs: [ - { indexed: true, internalType: 'address', name: 'from', type: 'address' }, - { indexed: true, internalType: 'address', name: 'to', type: 'address' }, - { - indexed: false, - internalType: 'uint256', - name: 'value', - type: 'uint256', - }, - ], - name: 'Transfer', - type: 'event', - }, - { - inputs: [ - { internalType: 'address', name: 'owner', type: 'address' }, - { internalType: 'address', name: 'spender', type: 'address' }, - ], - name: 'allowance', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'spender', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'approve', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [{ internalType: 'address', name: 'account', type: 'address' }], - name: 'balanceOf', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'decimals', - outputs: [{ internalType: 'uint8', name: '', type: 'uint8' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'name', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'symbol', - outputs: [{ internalType: 'string', name: '', type: 'string' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'totalSupply', - outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'transfer', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [ - { internalType: 'address', name: 'from', type: 'address' }, - { internalType: 'address', name: 'to', type: 'address' }, - { internalType: 'uint256', name: 'amount', type: 'uint256' }, - ], - name: 'transferFrom', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'nonpayable', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxy.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxy.ts.tpl deleted file mode 100644 index 32e6ffe7..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxy.ts.tpl +++ /dev/null @@ -1,41 +0,0 @@ -export const UpdateReservesProxy = [ - { - inputs: [{ internalType: 'address', name: '_reserveManager', type: 'address' }], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - inputs: [{ internalType: 'bytes10', name: 'workflowName', type: 'bytes10' }], - name: 'UnauthorizedWorkflowName', - type: 'error', - }, - { - inputs: [{ internalType: 'address', name: 'workflowOwner', type: 'address' }], - name: 'UnauthorizedWorkflowOwner', - type: 'error', - }, - { - inputs: [ - { internalType: 'bytes', name: 'metadata', type: 'bytes' }, - { internalType: 'bytes', name: 'report', type: 'bytes' }, - ], - name: 'onReport', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'reserveManager', - outputs: [{ internalType: 'contract IReserveManager', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'pure', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxySimplified.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxySimplified.ts.tpl deleted file mode 100644 index 611c2eb6..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/UpdateReservesProxySimplified.ts.tpl +++ /dev/null @@ -1,69 +0,0 @@ -export const UpdateReservesProxySimplified = [ - { - inputs: [ - { internalType: 'address', name: '_reserveManager', type: 'address' }, - { internalType: 'address', name: 'expectedAuthor', type: 'address' }, - { - internalType: 'bytes10', - name: 'expectedWorkflowName', - type: 'bytes10', - }, - ], - stateMutability: 'nonpayable', - type: 'constructor', - }, - { - inputs: [ - { internalType: 'address', name: 'received', type: 'address' }, - { internalType: 'address', name: 'expected', type: 'address' }, - ], - name: 'InvalidAuthor', - type: 'error', - }, - { - inputs: [ - { internalType: 'bytes10', name: 'received', type: 'bytes10' }, - { internalType: 'bytes10', name: 'expected', type: 'bytes10' }, - ], - name: 'InvalidWorkflowName', - type: 'error', - }, - { - inputs: [], - name: 'EXPECTED_AUTHOR', - outputs: [{ internalType: 'address', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [], - name: 'EXPECTED_WORKFLOW_NAME', - outputs: [{ internalType: 'bytes10', name: '', type: 'bytes10' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [ - { internalType: 'bytes', name: 'metadata', type: 'bytes' }, - { internalType: 'bytes', name: 'report', type: 'bytes' }, - ], - name: 'onReport', - outputs: [], - stateMutability: 'nonpayable', - type: 'function', - }, - { - inputs: [], - name: 'reserveManager', - outputs: [{ internalType: 'contract IReserveManager', name: '', type: 'address' }], - stateMutability: 'view', - type: 'function', - }, - { - inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }], - name: 'supportsInterface', - outputs: [{ internalType: 'bool', name: '', type: 'bool' }], - stateMutability: 'pure', - type: 'function', - }, -] as const diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/index.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/index.ts.tpl deleted file mode 100644 index d4264edd..00000000 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/abi/index.ts.tpl +++ /dev/null @@ -1,12 +0,0 @@ -export * from './BalanceReader' -export * from './IERC20' -export * from './IERC165' -export * from './IReceiver' -export * from './IReceiverTemplate' -export * from './IReserveManager' -export * from './ITypeAndVersion' -export * from './MessageEmitter' -export * from './ReserveManager' -export * from './SimpleERC20' -export * from './UpdateReservesProxy' -export * from './UpdateReservesProxySimplified' diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/BalanceReader.abi b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/BalanceReader.abi new file mode 100644 index 00000000..778be2d2 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/BalanceReader.abi @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address[]","name":"addresses","type":"address[]"}],"name":"getNativeBalances","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}] diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/IERC20.abi b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/IERC20.abi new file mode 100644 index 00000000..b2dee76b --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/IERC20.abi @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/MessageEmitter.abi b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/MessageEmitter.abi new file mode 100644 index 00000000..364a39e5 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/MessageEmitter.abi @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"emitter","type":"address"},{"indexed":true,"internalType":"uint256","name":"timestamp","type":"uint256"},{"indexed":false,"internalType":"string","name":"message","type":"string"}],"name":"MessageEmitted","type":"event"},{"inputs":[{"internalType":"string","name":"message","type":"string"}],"name":"emitMessage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"emitter","type":"address"}],"name":"getLastMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"emitter","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"getMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}] diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/ReserveManager.abi b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/ReserveManager.abi new file mode 100644 index 00000000..5cc08c9b --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/src/abi/ReserveManager.abi @@ -0,0 +1 @@ +[{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"requestId","type":"uint256"}],"name":"RequestReserveUpdate","type":"event"},{"inputs":[],"name":"lastTotalMinted","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastTotalReserve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"totalMinted","type":"uint256"},{"internalType":"uint256","name":"totalReserve","type":"uint256"}],"internalType":"struct UpdateReserves","name":"updateReserves","type":"tuple"}],"name":"updateReserves","outputs":[],"stateMutability":"nonpayable","type":"function"}] diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/BalanceReader.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/BalanceReader.ts.tpl new file mode 100644 index 00000000..227a8ef8 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/BalanceReader.ts.tpl @@ -0,0 +1,87 @@ +// Code generated — DO NOT EDIT. +import { decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address, Hex } from 'viem' +import { + bytesToHex, + encodeCallMsg, + EVMClient, + hexToBase64, + LAST_FINALIZED_BLOCK_NUMBER, + prepareReportRequest, + type Runtime, +} from '@chainlink/cre-sdk' + + +export const BalanceReaderABI = [{"inputs":[{"internalType":"address[]","name":"addresses","type":"address[]"}],"name":"getNativeBalances","outputs":[{"internalType":"uint256[]","name":"","type":"uint256[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}] as const + +export class BalanceReader { + constructor( + private readonly client: EVMClient, + public readonly address: Address, + ) {} + + getNativeBalances( + runtime: Runtime, + addresses: readonly `0x${string}`[], + ): readonly bigint[] { + const callData = encodeFunctionData({ + abi: BalanceReaderABI, + functionName: 'getNativeBalances' as const, + args: [addresses], + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: BalanceReaderABI, + functionName: 'getNativeBalances' as const, + data: bytesToHex(result.data), + }) as readonly bigint[] + } + + typeAndVersion( + runtime: Runtime, + ): string { + const callData = encodeFunctionData({ + abi: BalanceReaderABI, + functionName: 'typeAndVersion' as const, + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: BalanceReaderABI, + functionName: 'typeAndVersion' as const, + data: bytesToHex(result.data), + }) as string + } + + writeReport( + runtime: Runtime, + callData: Hex, + gasConfig?: { gasLimit?: string }, + ) { + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/BalanceReader_mock.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/BalanceReader_mock.ts.tpl new file mode 100644 index 00000000..77ce8911 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/BalanceReader_mock.ts.tpl @@ -0,0 +1,15 @@ +// Code generated — DO NOT EDIT. +import type { Address } from 'viem' +import { addContractMock, type ContractMock, type EvmMock } from '@chainlink/cre-sdk/test' + +import { BalanceReaderABI } from './BalanceReader' + +export type BalanceReaderMock = { + getNativeBalances?: (addresses: readonly `0x${string}`[]) => readonly bigint[] + typeAndVersion?: () => string +} & Pick, 'writeReport'> + +export function newBalanceReaderMock(address: Address, evmMock: EvmMock): BalanceReaderMock { + return addContractMock(evmMock, { address, abi: BalanceReaderABI }) as BalanceReaderMock +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/IERC20.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/IERC20.ts.tpl new file mode 100644 index 00000000..bbd7b3c5 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/IERC20.ts.tpl @@ -0,0 +1,188 @@ +// Code generated — DO NOT EDIT. +import { decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address, Hex } from 'viem' +import { + bytesToHex, + encodeCallMsg, + EVMClient, + hexToBase64, + LAST_FINALIZED_BLOCK_NUMBER, + prepareReportRequest, + type Runtime, +} from '@chainlink/cre-sdk' + + +export const IERC20ABI = [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"}] as const + +export class IERC20 { + constructor( + private readonly client: EVMClient, + public readonly address: Address, + ) {} + + allowance( + runtime: Runtime, + owner: `0x${string}`, + spender: `0x${string}`, + ): bigint { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'allowance' as const, + args: [owner, spender], + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: IERC20ABI, + functionName: 'allowance' as const, + data: bytesToHex(result.data), + }) as bigint + } + + balanceOf( + runtime: Runtime, + account: `0x${string}`, + ): bigint { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'balanceOf' as const, + args: [account], + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: IERC20ABI, + functionName: 'balanceOf' as const, + data: bytesToHex(result.data), + }) as bigint + } + + totalSupply( + runtime: Runtime, + ): bigint { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'totalSupply' as const, + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: IERC20ABI, + functionName: 'totalSupply' as const, + data: bytesToHex(result.data), + }) as bigint + } + + writeReportFromApprove( + runtime: Runtime, + spender: `0x${string}`, + amount: bigint, + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'approve' as const, + args: [spender, amount], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + + writeReportFromTransfer( + runtime: Runtime, + recipient: `0x${string}`, + amount: bigint, + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'transfer' as const, + args: [recipient, amount], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + + writeReportFromTransferFrom( + runtime: Runtime, + sender: `0x${string}`, + recipient: `0x${string}`, + amount: bigint, + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: IERC20ABI, + functionName: 'transferFrom' as const, + args: [sender, recipient, amount], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + + writeReport( + runtime: Runtime, + callData: Hex, + gasConfig?: { gasLimit?: string }, + ) { + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/IERC20_mock.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/IERC20_mock.ts.tpl new file mode 100644 index 00000000..547fdb0c --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/IERC20_mock.ts.tpl @@ -0,0 +1,16 @@ +// Code generated — DO NOT EDIT. +import type { Address } from 'viem' +import { addContractMock, type ContractMock, type EvmMock } from '@chainlink/cre-sdk/test' + +import { IERC20ABI } from './IERC20' + +export type IERC20Mock = { + allowance?: (owner: `0x${string}`, spender: `0x${string}`) => bigint + balanceOf?: (account: `0x${string}`) => bigint + totalSupply?: () => bigint +} & Pick, 'writeReport'> + +export function newIERC20Mock(address: Address, evmMock: EvmMock): IERC20Mock { + return addContractMock(evmMock, { address, abi: IERC20ABI }) as IERC20Mock +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/MessageEmitter.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/MessageEmitter.ts.tpl new file mode 100644 index 00000000..2f172226 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/MessageEmitter.ts.tpl @@ -0,0 +1,136 @@ +// Code generated — DO NOT EDIT. +import { decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address, Hex } from 'viem' +import { + bytesToHex, + encodeCallMsg, + EVMClient, + hexToBase64, + LAST_FINALIZED_BLOCK_NUMBER, + prepareReportRequest, + type Runtime, +} from '@chainlink/cre-sdk' + + +export const MessageEmitterABI = [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"emitter","type":"address"},{"indexed":true,"internalType":"uint256","name":"timestamp","type":"uint256"},{"indexed":false,"internalType":"string","name":"message","type":"string"}],"name":"MessageEmitted","type":"event"},{"inputs":[{"internalType":"string","name":"message","type":"string"}],"name":"emitMessage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"emitter","type":"address"}],"name":"getLastMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"emitter","type":"address"},{"internalType":"uint256","name":"timestamp","type":"uint256"}],"name":"getMessage","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"typeAndVersion","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}] as const + +export class MessageEmitter { + constructor( + private readonly client: EVMClient, + public readonly address: Address, + ) {} + + getLastMessage( + runtime: Runtime, + emitter: `0x${string}`, + ): string { + const callData = encodeFunctionData({ + abi: MessageEmitterABI, + functionName: 'getLastMessage' as const, + args: [emitter], + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: MessageEmitterABI, + functionName: 'getLastMessage' as const, + data: bytesToHex(result.data), + }) as string + } + + getMessage( + runtime: Runtime, + emitter: `0x${string}`, + timestamp: bigint, + ): string { + const callData = encodeFunctionData({ + abi: MessageEmitterABI, + functionName: 'getMessage' as const, + args: [emitter, timestamp], + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: MessageEmitterABI, + functionName: 'getMessage' as const, + data: bytesToHex(result.data), + }) as string + } + + typeAndVersion( + runtime: Runtime, + ): string { + const callData = encodeFunctionData({ + abi: MessageEmitterABI, + functionName: 'typeAndVersion' as const, + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: MessageEmitterABI, + functionName: 'typeAndVersion' as const, + data: bytesToHex(result.data), + }) as string + } + + writeReportFromEmitMessage( + runtime: Runtime, + message: string, + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: MessageEmitterABI, + functionName: 'emitMessage' as const, + args: [message], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + + writeReport( + runtime: Runtime, + callData: Hex, + gasConfig?: { gasLimit?: string }, + ) { + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/MessageEmitter_mock.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/MessageEmitter_mock.ts.tpl new file mode 100644 index 00000000..66d32393 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/MessageEmitter_mock.ts.tpl @@ -0,0 +1,16 @@ +// Code generated — DO NOT EDIT. +import type { Address } from 'viem' +import { addContractMock, type ContractMock, type EvmMock } from '@chainlink/cre-sdk/test' + +import { MessageEmitterABI } from './MessageEmitter' + +export type MessageEmitterMock = { + getLastMessage?: (emitter: `0x${string}`) => string + getMessage?: (emitter: `0x${string}`, timestamp: bigint) => string + typeAndVersion?: () => string +} & Pick, 'writeReport'> + +export function newMessageEmitterMock(address: Address, evmMock: EvmMock): MessageEmitterMock { + return addContractMock(evmMock, { address, abi: MessageEmitterABI }) as MessageEmitterMock +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/ReserveManager.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/ReserveManager.ts.tpl new file mode 100644 index 00000000..c49b35ff --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/ReserveManager.ts.tpl @@ -0,0 +1,109 @@ +// Code generated — DO NOT EDIT. +import { decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address, Hex } from 'viem' +import { + bytesToHex, + encodeCallMsg, + EVMClient, + hexToBase64, + LAST_FINALIZED_BLOCK_NUMBER, + prepareReportRequest, + type Runtime, +} from '@chainlink/cre-sdk' + + +export const ReserveManagerABI = [{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"requestId","type":"uint256"}],"name":"RequestReserveUpdate","type":"event"},{"inputs":[],"name":"lastTotalMinted","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastTotalReserve","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"internalType":"uint256","name":"totalMinted","type":"uint256"},{"internalType":"uint256","name":"totalReserve","type":"uint256"}],"internalType":"structUpdateReserves","name":"updateReserves","type":"tuple"}],"name":"updateReserves","outputs":[],"stateMutability":"nonpayable","type":"function"}] as const + +export class ReserveManager { + constructor( + private readonly client: EVMClient, + public readonly address: Address, + ) {} + + lastTotalMinted( + runtime: Runtime, + ): bigint { + const callData = encodeFunctionData({ + abi: ReserveManagerABI, + functionName: 'lastTotalMinted' as const, + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: ReserveManagerABI, + functionName: 'lastTotalMinted' as const, + data: bytesToHex(result.data), + }) as bigint + } + + lastTotalReserve( + runtime: Runtime, + ): bigint { + const callData = encodeFunctionData({ + abi: ReserveManagerABI, + functionName: 'lastTotalReserve' as const, + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: ReserveManagerABI, + functionName: 'lastTotalReserve' as const, + data: bytesToHex(result.data), + }) as bigint + } + + writeReportFromUpdateReserves( + runtime: Runtime, + updateReserves: { totalMinted: bigint; totalReserve: bigint }, + gasConfig?: { gasLimit?: string }, + ) { + const callData = encodeFunctionData({ + abi: ReserveManagerABI, + functionName: 'updateReserves' as const, + args: [updateReserves], + }) + + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + + writeReport( + runtime: Runtime, + callData: Hex, + gasConfig?: { gasLimit?: string }, + ) { + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/ReserveManager_mock.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/ReserveManager_mock.ts.tpl new file mode 100644 index 00000000..bf21326c --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/ReserveManager_mock.ts.tpl @@ -0,0 +1,15 @@ +// Code generated — DO NOT EDIT. +import type { Address } from 'viem' +import { addContractMock, type ContractMock, type EvmMock } from '@chainlink/cre-sdk/test' + +import { ReserveManagerABI } from './ReserveManager' + +export type ReserveManagerMock = { + lastTotalMinted?: () => bigint + lastTotalReserve?: () => bigint +} & Pick, 'writeReport'> + +export function newReserveManagerMock(address: Address, evmMock: EvmMock): ReserveManagerMock { + return addContractMock(evmMock, { address, abi: ReserveManagerABI }) as ReserveManagerMock +} + diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/index.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/index.ts.tpl new file mode 100644 index 00000000..b6e060bb --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/evm/ts/generated/index.ts.tpl @@ -0,0 +1,9 @@ +// Code generated — DO NOT EDIT. +export * from './BalanceReader' +export * from './BalanceReader_mock' +export * from './IERC20' +export * from './IERC20_mock' +export * from './MessageEmitter' +export * from './MessageEmitter_mock' +export * from './ReserveManager' +export * from './ReserveManager_mock' diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/package.json b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/package.json new file mode 100644 index 00000000..c389cb42 --- /dev/null +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/contracts/package.json @@ -0,0 +1,10 @@ +{ + "name": "contracts", + "version": "1.0.0", + "private": true, + "type": "module", + "dependencies": { + "@chainlink/cre-sdk": "^1.1.2", + "viem": "2.34.0" + } +} diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl index c44aa6e6..68e3e30b 100644 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.test.ts.tpl @@ -8,8 +8,11 @@ import { } from "@chainlink/cre-sdk/test"; import { initWorkflow, onCronTrigger, onLogTrigger, fetchReserveInfo } from "./main"; import type { Config } from "./main"; -import { type Address, decodeFunctionData, encodeFunctionData, encodeFunctionResult } from "viem"; -import { BalanceReader, IERC20, MessageEmitter } from "../contracts/abi"; +import type { Address } from "viem"; +import { newBalanceReaderMock } from "../contracts/evm/ts/generated/BalanceReader_mock"; +import { newIERC20Mock } from "../contracts/evm/ts/generated/IERC20_mock"; +import { newMessageEmitterMock } from "../contracts/evm/ts/generated/MessageEmitter_mock"; +import { newReserveManagerMock } from "../contracts/evm/ts/generated/ReserveManager_mock"; const mockConfig: Config = { schedule: "0 0 * * *", @@ -48,95 +51,28 @@ const setupEVMMocks = (config: Config) => { const evmMock = EvmMock.testInstance(network.chainSelector.selector); - // Mock contract calls - route based on target address and function signature - evmMock.callContract = (req) => { - const toAddress = Buffer.from(req.call?.to || new Uint8Array()).toString("hex").toLowerCase(); - const callData = Buffer.from(req.call?.data || new Uint8Array()); - - // BalanceReader.getNativeBalances - if (toAddress === config.evms[0].balanceReaderAddress.slice(2).toLowerCase()) { - const decoded = decodeFunctionData({ - abi: BalanceReader, - data: `0x${callData.toString("hex")}` as Address, - }); - - if (decoded.functionName === "getNativeBalances") { - const addresses = decoded.args[0] as Address[]; - expect(addresses.length).toBeGreaterThan(0); - - // Return mock balance for each address (0.5 ETH in wei) - const mockBalances = addresses.map(() => 500000000000000000n); - const resultData = encodeFunctionResult({ - abi: BalanceReader, - functionName: "getNativeBalances", - result: mockBalances, - }); - - return { - data: Buffer.from(resultData.slice(2), "hex"), - }; - } - } - - // IERC20.totalSupply - if (toAddress === config.evms[0].tokenAddress.slice(2).toLowerCase()) { - const decoded = decodeFunctionData({ - abi: IERC20, - data: `0x${callData.toString("hex")}` as Address, - }); - - if (decoded.functionName === "totalSupply") { - // Return mock total supply (1 token with 18 decimals) - const mockSupply = 1000000000000000000n; - const resultData = encodeFunctionResult({ - abi: IERC20, - functionName: "totalSupply", - result: mockSupply, - }); - - return { - data: Buffer.from(resultData.slice(2), "hex"), - }; - } - } - - // MessageEmitter.getLastMessage - if (toAddress === config.evms[0].messageEmitterAddress.slice(2).toLowerCase()) { - const decoded = decodeFunctionData({ - abi: MessageEmitter, - data: `0x${callData.toString("hex")}` as Address, - }); - - if (decoded.functionName === "getLastMessage") { - // Verify the emitter address parameter is passed correctly - const emitterArg = decoded.args[0] as string; - expect(emitterArg).toBeDefined(); - - const mockMessage = "Test message from contract"; - const resultData = encodeFunctionResult({ - abi: MessageEmitter, - functionName: "getLastMessage", - result: mockMessage, - }); - - return { - data: Buffer.from(resultData.slice(2), "hex"), - }; - } - } - - throw new Error(`Unmocked contract call to ${toAddress} with data ${callData.toString("hex")}`); + // BalanceReader.getNativeBalances - returns mock native token balances (0.5 ETH in wei) + const balanceMock = newBalanceReaderMock(config.evms[0].balanceReaderAddress as Address, evmMock); + balanceMock.getNativeBalances = (addresses: readonly Address[]) => { + expect(addresses.length).toBeGreaterThan(0); + return addresses.map(() => 500000000000000000n); }; - // Mock writeReport for updateReserves - evmMock.writeReport = (req) => { - // Convert Uint8Array receiver to hex string for comparison - const receiverHex = `0x${Buffer.from(req.receiver || new Uint8Array()).toString("hex")}`; - expect(receiverHex.toLowerCase()).toBe(config.evms[0].proxyAddress.toLowerCase()); - expect(req.report).toBeDefined(); - // gasLimit is bigint, config has string - compare the values - expect(req.gasConfig?.gasLimit?.toString()).toBe(config.evms[0].gasLimit); + // IERC20.totalSupply - returns mock total supply (1 token with 18 decimals) + const erc20Mock = newIERC20Mock(config.evms[0].tokenAddress as Address, evmMock); + erc20Mock.totalSupply = () => 1000000000000000000n; + // MessageEmitter.getLastMessage - returns mock message (for log trigger) + const messageMock = newMessageEmitterMock(config.evms[0].messageEmitterAddress as Address, evmMock); + messageMock.getLastMessage = (emitter: Address) => { + expect(emitter).toBeDefined(); + return "Test message from contract"; + }; + + // ReserveManager - mock writeReport for updateReserves + const reserveMock = newReserveManagerMock(config.evms[0].proxyAddress as Address, evmMock); + reserveMock.writeReport = (req) => { + expect(req.gasConfig.gasLimit?.toString()).toBe(config.evms[0].gasLimit); return { txStatus: TxStatus.SUCCESS, txHash: new Uint8Array(Buffer.from("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", "hex")), diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl index 9f14c7f2..90f48982 100644 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/main.ts.tpl @@ -7,19 +7,19 @@ import { EVMClient, HTTPClient, type EVMLog, - encodeCallMsg, getNetwork, type HTTPSendRequester, - hexToBase64, - LAST_FINALIZED_BLOCK_NUMBER, median, Runner, type Runtime, TxStatus, } from '@chainlink/cre-sdk' -import { type Address, decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address } from 'viem' import { z } from 'zod' -import { BalanceReader, IERC20, MessageEmitter, ReserveManager } from '../contracts/abi' +import { BalanceReader } from '../contracts/evm/ts/generated/BalanceReader' +import { IERC20 } from '../contracts/evm/ts/generated/IERC20' +import { MessageEmitter } from '../contracts/evm/ts/generated/MessageEmitter' +import { ReserveManager } from '../contracts/evm/ts/generated/ReserveManager' const configSchema = z.object({ schedule: z.string(), @@ -52,7 +52,6 @@ interface ReserveInfo { totalReserve: number } -// Utility function to safely stringify objects with bigints const safeJsonStringify = (obj: any): string => JSON.stringify(obj, (_, value) => (typeof value === 'bigint' ? value.toString() : value), 2) @@ -92,31 +91,9 @@ const fetchNativeTokenBalance = ( } const evmClient = new EVMClient(network.chainSelector.selector) + const balanceReader = new BalanceReader(evmClient, evmConfig.balanceReaderAddress as Address) - // Encode the contract call data for getNativeBalances - const callData = encodeFunctionData({ - abi: BalanceReader, - functionName: 'getNativeBalances', - args: [[tokenHolderAddress as Address]], - }) - - const contractCall = evmClient - .callContract(runtime, { - call: encodeCallMsg({ - from: zeroAddress, - to: evmConfig.balanceReaderAddress as Address, - data: callData, - }), - blockNumber: LAST_FINALIZED_BLOCK_NUMBER, - }) - .result() - - // Decode the result - const balances = decodeFunctionResult({ - abi: BalanceReader, - functionName: 'getNativeBalances', - data: bytesToHex(contractCall.data), - }) + const balances = balanceReader.getNativeBalances(runtime, [tokenHolderAddress as Address]) if (!balances || balances.length === 0) { throw new Error('No balances returned from contract') @@ -141,31 +118,9 @@ const getTotalSupply = (runtime: Runtime): bigint => { } const evmClient = new EVMClient(network.chainSelector.selector) + const ierc20 = new IERC20(evmClient, evmConfig.tokenAddress as Address) - // Encode the contract call data for totalSupply - const callData = encodeFunctionData({ - abi: IERC20, - functionName: 'totalSupply', - }) - - const contractCall = evmClient - .callContract(runtime, { - call: encodeCallMsg({ - from: zeroAddress, - to: evmConfig.tokenAddress as Address, - data: callData, - }), - blockNumber: LAST_FINALIZED_BLOCK_NUMBER, - }) - .result() - - // Decode the result - const supply = decodeFunctionResult({ - abi: IERC20, - functionName: 'totalSupply', - data: bytesToHex(contractCall.data), - }) - + const supply = ierc20.totalSupply(runtime) totalSupply += supply } @@ -189,42 +144,17 @@ const updateReserves = ( } const evmClient = new EVMClient(network.chainSelector.selector) + const proxy = new ReserveManager(evmClient, evmConfig.proxyAddress as Address) runtime.log( `Updating reserves totalSupply ${totalSupply.toString()} totalReserveScaled ${totalReserveScaled.toString()}`, ) - // Encode the contract call data for updateReserves - const callData = encodeFunctionData({ - abi: ReserveManager, - functionName: 'updateReserves', - args: [ - { - totalMinted: totalSupply, - totalReserve: totalReserveScaled, - }, - ], - }) - - // Step 1: Generate report using consensus capability - const reportResponse = runtime - .report({ - encodedPayload: hexToBase64(callData), - encoderName: 'evm', - signingAlgo: 'ecdsa', - hashingAlgo: 'keccak256', - }) - .result() - - const resp = evmClient - .writeReport(runtime, { - receiver: evmConfig.proxyAddress, - report: reportResponse, - gasConfig: { - gasLimit: evmConfig.gasLimit, - }, - }) - .result() + const resp = proxy.writeReportFromUpdateReserves( + runtime, + { totalMinted: totalSupply, totalReserve: totalReserveScaled }, + { gasLimit: evmConfig.gasLimit }, + ) const txStatus = resp.txStatus @@ -290,33 +220,9 @@ const getLastMessage = ( } const evmClient = new EVMClient(network.chainSelector.selector) + const messageEmitter = new MessageEmitter(evmClient, evmConfig.messageEmitterAddress as Address) - // Encode the contract call data for getLastMessage - const callData = encodeFunctionData({ - abi: MessageEmitter, - functionName: 'getLastMessage', - args: [emitter as Address], - }) - - const contractCall = evmClient - .callContract(runtime, { - call: encodeCallMsg({ - from: zeroAddress, - to: evmConfig.messageEmitterAddress as Address, - data: callData, - }), - blockNumber: LAST_FINALIZED_BLOCK_NUMBER, - }) - .result() - - // Decode the result - const message = decodeFunctionResult({ - abi: MessageEmitter, - functionName: 'getLastMessage', - data: bytesToHex(contractCall.data), - }) - - return message + return messageEmitter.getLastMessage(runtime, emitter as Address) } export const onCronTrigger = (runtime: Runtime, payload: CronPayload): string => { @@ -339,7 +245,6 @@ export const onLogTrigger = (runtime: Runtime, payload: EVMLog): string throw new Error(`log payload does not contain enough topics ${topics.length}`) } - // topics[1] is a 32-byte topic, but the address is the last 20 bytes const emitter = bytesToHex(topics[1].slice(12)) runtime.log(`Emitter ${emitter}`) diff --git a/cmd/creinit/template/workflow/typescriptPorExampleDev/package.json.tpl b/cmd/creinit/template/workflow/typescriptPorExampleDev/package.json.tpl index e613002f..4f0b1573 100644 --- a/cmd/creinit/template/workflow/typescriptPorExampleDev/package.json.tpl +++ b/cmd/creinit/template/workflow/typescriptPorExampleDev/package.json.tpl @@ -8,7 +8,7 @@ }, "license": "UNLICENSED", "dependencies": { - "@chainlink/cre-sdk": "^1.1.1", + "@chainlink/cre-sdk": "^1.1.2", "viem": "2.34.0", "zod": "3.25.76" }, diff --git a/cmd/creinit/template/workflow/typescriptSimpleExample/package.json.tpl b/cmd/creinit/template/workflow/typescriptSimpleExample/package.json.tpl index 0dc43de0..e731fde0 100644 --- a/cmd/creinit/template/workflow/typescriptSimpleExample/package.json.tpl +++ b/cmd/creinit/template/workflow/typescriptSimpleExample/package.json.tpl @@ -8,7 +8,7 @@ }, "license": "UNLICENSED", "dependencies": { - "@chainlink/cre-sdk": "^1.1.1" + "@chainlink/cre-sdk": "^1.1.2" }, "devDependencies": { "@types/bun": "1.2.21" diff --git a/cmd/generate-bindings/bindings/abigen/bindv2.go b/cmd/generate-bindings/bindings/abigen/bindv2.go index da001dfe..cbf7d642 100644 --- a/cmd/generate-bindings/bindings/abigen/bindv2.go +++ b/cmd/generate-bindings/bindings/abigen/bindv2.go @@ -27,6 +27,7 @@ import ( "regexp" "slices" "sort" + "strconv" "strings" "text/template" "unicode" @@ -406,3 +407,142 @@ func isDynTopicType(t abi.Type) bool { return false } } + +func tsBindBasicType(kind abi.Type) string { + switch kind.T { + case abi.AddressTy: + return "`0x${string}`" + case abi.IntTy, abi.UintTy: + parts := regexp.MustCompile(`(u)?int([0-9]*)`).FindStringSubmatch(kind.String()) + bits := 256 + if len(parts) >= 3 && parts[2] != "" { + bits, _ = strconv.Atoi(parts[2]) + } + if bits <= 48 { + return "number" + } + return "bigint" + case abi.BoolTy: + return "boolean" + case abi.StringTy: + return "string" + case abi.FixedBytesTy, abi.BytesTy, abi.HashTy, abi.FunctionTy: + return "`0x${string}`" + default: + return "unknown" + } +} + +func tsReturnType(outputs abi.Arguments, structs map[string]*tmplStruct) string { + if len(outputs) == 0 { + return "void" + } + if len(outputs) == 1 { + return tsBindType(outputs[0].Type, structs) + } + var types []string + for _, output := range outputs { + types = append(types, tsBindType(output.Type, structs)) + } + return "readonly [" + strings.Join(types, ", ") + "]" +} + +func tsBindType(kind abi.Type, structs map[string]*tmplStruct) string { + switch kind.T { + case abi.TupleTy: + s := structs[kind.TupleRawName+kind.String()] + if s == nil { + return "unknown" + } + var fields []string + for _, f := range s.Fields { + fields = append(fields, fmt.Sprintf("%s: %s", decapitalise(f.Name), tsBindType(f.SolKind, structs))) + } + return "{ " + strings.Join(fields, "; ") + " }" + case abi.ArrayTy: + return "readonly " + tsBindType(*kind.Elem, structs) + "[]" + case abi.SliceTy: + return "readonly " + tsBindType(*kind.Elem, structs) + "[]" + default: + return tsBindBasicType(kind) + } +} + +// 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) { + b := binder{ + contracts: make(map[string]*tmplContractV2), + structs: make(map[string]*tmplStruct), + aliases: aliases, + } + for i := 0; i < len(types); i++ { + evmABI, err := abi.JSON(strings.NewReader(abis[i])) + if err != nil { + return "", err + } + + for _, input := range evmABI.Constructor.Inputs { + if hasStruct(input.Type) { + bindStructType(input.Type, b.structs) + } + } + + cb := newContractBinder(&b) + err = iterSorted(evmABI.Methods, func(_ string, original abi.Method) error { + return cb.bindMethod(original) + }) + if err != nil { + return "", err + } + err = iterSorted(evmABI.Events, func(_ string, original abi.Event) error { + return cb.bindEvent(original) + }) + if err != nil { + return "", err + } + err = iterSorted(evmABI.Errors, func(_ string, original abi.Error) error { + return cb.bindError(original) + }) + if err != nil { + return "", err + } + b.contracts[types[i]] = newTmplContractV2(types[i], abis[i], bytecodes[i], evmABI.Constructor, cb) + } + + invertedLibs := make(map[string]string) + for pattern, name := range libs { + invertedLibs[name] = pattern + } + + sanitizeStructNames(b.structs, b.contracts) + + data := tmplDataV2{ + Package: pkg, + Contracts: b.contracts, + Libraries: invertedLibs, + Structs: b.structs, + } + + for typ, contract := range data.Contracts { + for _, depPattern := range parseLibraryDeps(contract.InputBin) { + data.Contracts[typ].Libraries[libs[depPattern]] = depPattern + } + } + buffer := new(bytes.Buffer) + funcs := map[string]interface{}{ + "bindtype": tsBindType, + "bindtopictype": tsBindType, + "returntype": tsReturnType, + "capitalise": abi.ToCamelCase, + "decapitalise": decapitalise, + "unescapeabi": func(s string) string { + return strings.ReplaceAll(s, "\\\"", "\"") + }, + } + tmpl := template.Must(template.New("").Funcs(funcs).Parse(templateContent)) + if err := tmpl.Execute(buffer, data); err != nil { + return "", err + } + return buffer.String(), nil +} diff --git a/cmd/generate-bindings/bindings/bindgen.go b/cmd/generate-bindings/bindings/bindgen.go index 593ed6dc..150fbcac 100644 --- a/cmd/generate-bindings/bindings/bindgen.go +++ b/cmd/generate-bindings/bindings/bindgen.go @@ -20,6 +20,12 @@ var tpl string //go:embed mockcontract.go.tpl var mockTpl string +//go:embed sourcecre.ts.tpl +var tsTpl string + +//go:embed mockcontract.ts.tpl +var tsMockTpl string + func GenerateBindings( combinedJSONPath string, // path to combined-json, or "" abiPath string, // path to a single ABI JSON, or "" @@ -109,3 +115,49 @@ func GenerateBindings( return nil } + +func GenerateBindingsTS( + abiPath string, + typeName string, + outPath string, +) error { + if abiPath == "" { + return errors.New("must provide abiPath") + } + + abiBytes, err := os.ReadFile(abiPath) + if err != nil { + return fmt.Errorf("read ABI %q: %w", abiPath, err) + } + if err := json.Unmarshal(abiBytes, new(interface{})); err != nil { + return fmt.Errorf("invalid ABI JSON %q: %w", abiPath, err) + } + + types := []string{typeName} + abis := []string{string(abiBytes)} + bins := []string{""} + + libs := make(map[string]string) + aliases := make(map[string]string) + + outSrc, err := abigen.BindV2TS(types, abis, bins, "", libs, aliases, tsTpl) + if err != nil { + return fmt.Errorf("BindV2TS: %w", err) + } + + if err := os.WriteFile(outPath, []byte(outSrc), 0o600); err != nil { + return fmt.Errorf("write %q: %w", outPath, err) + } + + mockSrc, err := abigen.BindV2TS(types, abis, bins, "", libs, aliases, tsMockTpl) + if err != nil { + return fmt.Errorf("BindV2TS mock: %w", err) + } + + mockPath := strings.TrimSuffix(outPath, ".ts") + "_mock.ts" + if err := os.WriteFile(mockPath, []byte(mockSrc), 0o600); err != nil { + return fmt.Errorf("write mock %q: %w", mockPath, err) + } + + return nil +} diff --git a/cmd/generate-bindings/bindings/mockcontract.ts.tpl b/cmd/generate-bindings/bindings/mockcontract.ts.tpl new file mode 100644 index 00000000..88792d63 --- /dev/null +++ b/cmd/generate-bindings/bindings/mockcontract.ts.tpl @@ -0,0 +1,18 @@ +// Code generated — DO NOT EDIT. +import type { Address } from 'viem' +import { addContractMock, type ContractMock, type EvmMock } from '@chainlink/cre-sdk/test' +{{range $contract := .Contracts}} +import { {{$contract.Type}}ABI } from './{{$contract.Type}}' + +export type {{$contract.Type}}Mock = { + {{- range $call := $contract.Calls}} + {{- if or $call.Original.Constant (eq $call.Original.StateMutability "view") (eq $call.Original.StateMutability "pure")}} + {{decapitalise $call.Normalized.Name}}?: ({{range $idx, $param := $call.Normalized.Inputs}}{{if $idx}}, {{end}}{{$param.Name}}: {{bindtype $param.Type $.Structs}}{{end}}) => {{returntype $call.Normalized.Outputs $.Structs}} + {{- end}} + {{- end}} +} & Pick, 'writeReport'> + +export function new{{$contract.Type}}Mock(address: Address, evmMock: EvmMock): {{$contract.Type}}Mock { + return addContractMock(evmMock, { address, abi: {{$contract.Type}}ABI }) as {{$contract.Type}}Mock +} +{{end}} diff --git a/cmd/generate-bindings/bindings/sourcecre.ts.tpl b/cmd/generate-bindings/bindings/sourcecre.ts.tpl new file mode 100644 index 00000000..45c2fb72 --- /dev/null +++ b/cmd/generate-bindings/bindings/sourcecre.ts.tpl @@ -0,0 +1,107 @@ +// Code generated — DO NOT EDIT. +import { decodeFunctionResult, encodeFunctionData, zeroAddress } from 'viem' +import type { Address, Hex } from 'viem' +import { + bytesToHex, + encodeCallMsg, + EVMClient, + hexToBase64, + LAST_FINALIZED_BLOCK_NUMBER, + prepareReportRequest, + type Runtime, +} from '@chainlink/cre-sdk' + +{{range $contract := .Contracts}} +export const {{$contract.Type}}ABI = {{unescapeabi .InputABI}} as const + +export class {{$contract.Type}} { + constructor( + private readonly client: EVMClient, + public readonly address: Address, + ) {} + + {{- range $call := $contract.Calls}} + {{- if or $call.Original.Constant (eq $call.Original.StateMutability "view") (eq $call.Original.StateMutability "pure")}} + + {{decapitalise $call.Normalized.Name}}( + runtime: Runtime, + {{- range $param := $call.Normalized.Inputs}} + {{$param.Name}}: {{bindtype $param.Type $.Structs}}, + {{- end}} + ): {{returntype $call.Normalized.Outputs $.Structs}} { + const callData = encodeFunctionData({ + abi: {{$contract.Type}}ABI, + functionName: '{{$call.Original.Name}}' as const, + {{- if gt (len $call.Normalized.Inputs) 0}} + args: [{{range $idx, $param := $call.Normalized.Inputs}}{{if $idx}}, {{end}}{{$param.Name}}{{end}}], + {{- end}} + }) + + const result = this.client + .callContract(runtime, { + call: encodeCallMsg({ from: zeroAddress, to: this.address, data: callData }), + blockNumber: LAST_FINALIZED_BLOCK_NUMBER, + }) + .result() + + return decodeFunctionResult({ + abi: {{$contract.Type}}ABI, + functionName: '{{$call.Original.Name}}' as const, + data: bytesToHex(result.data), + }) as {{returntype $call.Normalized.Outputs $.Structs}} + } + {{- end}} + {{- end}} + + {{- range $call := $contract.Calls}} + {{- if not (or $call.Original.Constant (eq $call.Original.StateMutability "view") (eq $call.Original.StateMutability "pure"))}} + {{- if gt (len $call.Normalized.Inputs) 0}} + + writeReportFrom{{capitalise $call.Normalized.Name}}( + runtime: Runtime, + {{- range $param := $call.Normalized.Inputs}} + {{$param.Name}}: {{bindtype $param.Type $.Structs}}, + {{- 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 reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } + {{- end}} + {{- end}} + {{- end}} + + writeReport( + runtime: Runtime, + callData: Hex, + gasConfig?: { gasLimit?: string }, + ) { + const reportResponse = runtime + .report(prepareReportRequest(callData)) + .result() + + return this.client + .writeReport(runtime, { + receiver: this.address, + report: reportResponse, + gasConfig, + }) + .result() + } +} +{{end}} diff --git a/cmd/generate-bindings/generate-bindings.go b/cmd/generate-bindings/generate-bindings.go index 47b6fcab..c87fa35d 100644 --- a/cmd/generate-bindings/generate-bindings.go +++ b/cmd/generate-bindings/generate-bindings.go @@ -5,6 +5,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -20,10 +21,12 @@ import ( type Inputs struct { ProjectRoot string `validate:"required,dir" cli:"--project-root"` ChainFamily string `validate:"required,oneof=evm" cli:"--chain-family"` - Language string `validate:"required,oneof=go" cli:"--language"` + GoLang bool `cli:"--go"` + TypeScript bool `cli:"--typescript"` AbiPath string `validate:"required,path_read" cli:"--abi"` PkgName string `validate:"required" cli:"--pkg"` - OutPath string `validate:"required" cli:"--out"` + GoOutPath string // contracts/{chain}/src/generated — set when GoLang is true + TSOutPath string // contracts/{chain}/ts/generated — set when TypeScript is true } func New(runtimeContext *runtime.Context) *cobra.Command { @@ -52,7 +55,8 @@ For example, IERC20.abi generates bindings in generated/ierc20/ package.`, } generateBindingsCmd.Flags().StringP("project-root", "p", "", "Path to project root directory (defaults to current directory)") - generateBindingsCmd.Flags().StringP("language", "l", "go", "Target language (go)") + generateBindingsCmd.Flags().Bool("go", false, "Generate Go bindings") + generateBindingsCmd.Flags().Bool("typescript", false, "Generate TypeScript bindings") generateBindingsCmd.Flags().StringP("abi", "a", "", "Path to ABI directory (defaults to contracts/{chain-family}/src/abi/)") generateBindingsCmd.Flags().StringP("pkg", "k", "bindings", "Base package name (each contract gets its own subdirectory)") @@ -71,6 +75,30 @@ func newHandler(ctx *runtime.Context) *handler { } } +func detectLanguages(projectRoot string) (goLang, typescript bool) { + _ = filepath.WalkDir(projectRoot, func(path string, d os.DirEntry, err error) error { + if err != nil { + return nil + } + if d.IsDir() { + // Skip node_modules and other dependency directories + if d.Name() == "node_modules" || d.Name() == ".git" { + return filepath.SkipDir + } + return nil + } + base := filepath.Base(path) + if strings.HasSuffix(base, ".go") { + goLang = true + } + if strings.HasSuffix(base, ".ts") && !strings.HasSuffix(base, ".d.ts") { + typescript = true + } + return nil + }) + return goLang, typescript +} + func (h *handler) ResolveInputs(args []string, v *viper.Viper) (Inputs, error) { // Get current working directory as default project root currentDir, err := os.Getwd() @@ -92,10 +120,17 @@ func (h *handler) ResolveInputs(args []string, v *viper.Viper) (Inputs, error) { // Chain family is now a positional argument chainFamily := args[0] - // Language defaults are handled by StringP - language := v.GetString("language") + // Resolve languages: flags take precedence, else auto-detect + goLang := v.GetBool("go") + typescript := v.GetBool("typescript") + if !goLang && !typescript { + goLang, typescript = detectLanguages(projectRoot) + } + if !goLang && !typescript { + return Inputs{}, fmt.Errorf("no target language specified and none detected (use --go and/or --typescript, or ensure project contains .go or .ts files)") + } - // Resolve ABI path with fallback to contracts/{chainFamily}/src/abi/ + // Unified ABI path for both languages: contracts/{chain}/src/abi abiPath := v.GetString("abi") if abiPath == "" { abiPath = filepath.Join(projectRoot, "contracts", chainFamily, "src", "abi") @@ -104,16 +139,24 @@ func (h *handler) ResolveInputs(args []string, v *viper.Viper) (Inputs, error) { // 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") + // Separate output paths: Go uses src/, TS uses ts/ (typescript convention) + var goOutPath, tsOutPath string + if goLang { + goOutPath = filepath.Join(projectRoot, "contracts", chainFamily, "src", "generated") + } + if typescript { + tsOutPath = filepath.Join(projectRoot, "contracts", chainFamily, "ts", "generated") + } return Inputs{ ProjectRoot: projectRoot, ChainFamily: chainFamily, - Language: language, + GoLang: goLang, + TypeScript: typescript, AbiPath: abiPath, PkgName: pkgName, - OutPath: outPath, + GoOutPath: goOutPath, + TSOutPath: tsOutPath, }, nil } @@ -135,17 +178,26 @@ func (h *handler) ValidateInputs(inputs Inputs) error { return fmt.Errorf("failed to access ABI path: %w", err) } - // Validate that if AbiPath is a directory, it contains .abi files + // Validate that if AbiPath is a directory, it contains ABI files (*.abi for both languages) if info, err := os.Stat(inputs.AbiPath); err == nil && info.IsDir() { - files, err := filepath.Glob(filepath.Join(inputs.AbiPath, "*.abi")) + abiExt := "*.abi" + files, err := filepath.Glob(filepath.Join(inputs.AbiPath, abiExt)) 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 fmt.Errorf("no %s files found in directory: %s", abiExt, inputs.AbiPath) } } + // Ensure at least one output path is set for the active language(s) + if inputs.GoLang && inputs.GoOutPath == "" { + return fmt.Errorf("Go output path is required when --go is set") + } + if inputs.TypeScript && inputs.TSOutPath == "" { + return fmt.Errorf("TypeScript output path is required when --typescript is set") + } + h.validated = true return nil } @@ -191,56 +243,88 @@ func contractNameToPackage(contractName string) string { } func (h *handler) processAbiDirectory(inputs Inputs) error { - // Read all .abi files in the directory - files, err := filepath.Glob(filepath.Join(inputs.AbiPath, "*.abi")) + abiExt := "*.abi" + files, err := filepath.Glob(filepath.Join(inputs.AbiPath, abiExt)) 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) + return fmt.Errorf("no %s files found in directory: %s", abiExt, 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) + if inputs.GoLang { + 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 } - packageNames[packageName] = true } + // Track generated files for TypeScript barrel export + var generatedContracts []string + // 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 + ext := filepath.Ext(contractName) + contractName = contractName[:len(contractName)-len(ext)] + + if inputs.TypeScript { + outputFile := filepath.Join(inputs.TSOutPath, contractName+".ts") + ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) + + err = bindings.GenerateBindingsTS( + abiFile, + contractName, + outputFile, + ) + if err != nil { + return fmt.Errorf("failed to generate TypeScript bindings for %s: %w", contractName, err) + } + generatedContracts = append(generatedContracts, contractName) + } - // Convert contract name to package name - packageName := contractNameToPackage(contractName) + if inputs.GoLang { + 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) - } + contractOutDir := filepath.Join(inputs.GoOutPath, 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") + outputFile := filepath.Join(contractOutDir, contractName+".go") + ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) - ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) + err = bindings.GenerateBindings( + "", + abiFile, + packageName, + contractName, + outputFile, + ) + if err != nil { + return fmt.Errorf("failed to generate bindings for %s: %w", contractName, err) + } + } + } - err = bindings.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) + // Generate barrel index.ts for TypeScript + if inputs.TypeScript && len(generatedContracts) > 0 { + indexPath := filepath.Join(inputs.TSOutPath, "index.ts") + var indexContent string + indexContent += "// Code generated — DO NOT EDIT.\n" + for _, name := range generatedContracts { + indexContent += fmt.Sprintf("export * from './%s'\n", name) + indexContent += fmt.Sprintf("export * from './%s_mock'\n", name) + } + if err := os.WriteFile(indexPath, []byte(indexContent), 0o600); err != nil { + return fmt.Errorf("failed to write index.ts: %w", err) } } @@ -248,52 +332,73 @@ func (h *handler) processAbiDirectory(inputs Inputs) error { } func (h *handler) processSingleAbi(inputs Inputs) 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 + ext := filepath.Ext(contractName) + if ext != "" { + contractName = contractName[:len(contractName)-len(ext)] } - // Convert contract name to package name - packageName := contractNameToPackage(contractName) + if inputs.TypeScript { + outputFile := filepath.Join(inputs.TSOutPath, contractName+".ts") + ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) - // 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) + if err := bindings.GenerateBindingsTS( + inputs.AbiPath, + contractName, + outputFile, + ); err != nil { + return err + } } - // Create output file path in contract-specific directory - outputFile := filepath.Join(contractOutDir, contractName+".go") + if inputs.GoLang { + packageName := contractNameToPackage(contractName) + + contractOutDir := filepath.Join(inputs.GoOutPath, packageName) + if err := os.MkdirAll(contractOutDir, 0o755); err != nil { + return fmt.Errorf("failed to create contract output directory %s: %w", contractOutDir, err) + } + + outputFile := filepath.Join(contractOutDir, contractName+".go") + ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) - ui.Dim(fmt.Sprintf("Processing: %s -> %s", contractName, outputFile)) + if err := bindings.GenerateBindings( + "", + inputs.AbiPath, + packageName, + contractName, + outputFile, + ); err != nil { + return err + } + } - return bindings.GenerateBindings( - "", // combinedJSONPath - empty for now - inputs.AbiPath, - packageName, // Use contract-specific package name - contractName, // Use contract name as type name - outputFile, - ) + return nil } func (h *handler) Execute(inputs Inputs) error { - ui.Dim(fmt.Sprintf("Project: %s, Chain: %s, Language: %s", inputs.ProjectRoot, inputs.ChainFamily, inputs.Language)) - - // Validate language - switch inputs.Language { - case "go": - // Language supported, continue - default: - return fmt.Errorf("unsupported language: %s", inputs.Language) + langs := []string{} + if inputs.GoLang { + langs = append(langs, "go") + } + if inputs.TypeScript { + langs = append(langs, "typescript") } + ui.Dim(fmt.Sprintf("Project: %s, Chain: %s, Languages: %v", inputs.ProjectRoot, inputs.ChainFamily, langs)) // 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) + // Create output directories for active language(s) + if inputs.GoLang { + if err := os.MkdirAll(inputs.GoOutPath, 0o755); err != nil { + return fmt.Errorf("failed to create Go output directory: %w", err) + } + } + if inputs.TypeScript { + if err := os.MkdirAll(inputs.TSOutPath, 0o755); err != nil { + return fmt.Errorf("failed to create TypeScript output directory: %w", err) + } } // Check if ABI path is a directory or file @@ -312,25 +417,28 @@ func (h *handler) Execute(inputs Inputs) error { } } - spinner := ui.NewSpinner() - spinner.Start("Installing dependencies...") + if inputs.GoLang { + spinner := ui.NewSpinner() + spinner.Start("Installing dependencies...") + + err = runCommand(inputs.ProjectRoot, "go", "get", "github.com/smartcontractkit/cre-sdk-go@"+creinit.SdkVersion) + if err != nil { + spinner.Stop() + return err + } + err = runCommand(inputs.ProjectRoot, "go", "get", "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm@"+creinit.EVMCapabilitiesVersion) + if err != nil { + spinner.Stop() + return err + } + if err = runCommand(inputs.ProjectRoot, "go", "mod", "tidy"); err != nil { + spinner.Stop() + return err + } - err = runCommand(inputs.ProjectRoot, "go", "get", "github.com/smartcontractkit/cre-sdk-go@"+creinit.SdkVersion) - if err != nil { - spinner.Stop() - return err - } - err = runCommand(inputs.ProjectRoot, "go", "get", "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm@"+creinit.EVMCapabilitiesVersion) - if err != nil { - spinner.Stop() - return err - } - if err = runCommand(inputs.ProjectRoot, "go", "mod", "tidy"); err != nil { spinner.Stop() - return err } - spinner.Stop() ui.Success("Bindings generated successfully") return nil default: diff --git a/cmd/generate-bindings/generate-bindings_test.go b/cmd/generate-bindings/generate-bindings_test.go index 140df93c..5f82a834 100644 --- a/cmd/generate-bindings/generate-bindings_test.go +++ b/cmd/generate-bindings/generate-bindings_test.go @@ -72,10 +72,10 @@ func TestResolveInputs_DefaultFallbacks(t *testing.T) { runtimeCtx := &runtime.Context{} handler := newHandler(runtimeCtx) - // Test with minimal input (only chain-family) + // Create a .go file for auto-detection (or use --go flag) v := viper.New() - v.Set("language", "go") // Default from StringP - v.Set("pkg", "bindings") // Default from StringP + v.Set("go", true) + v.Set("pkg", "bindings") inputs, err := handler.ResolveInputs([]string{"evm"}, v) require.NoError(t, err) @@ -85,14 +85,309 @@ func TestResolveInputs_DefaultFallbacks(t *testing.T) { actualRoot, _ := filepath.EvalSymlinks(inputs.ProjectRoot) assert.Equal(t, expectedRoot, actualRoot) assert.Equal(t, "evm", inputs.ChainFamily) - assert.Equal(t, "go", inputs.Language) + assert.True(t, inputs.GoLang) expectedAbi, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "src", "abi")) actualAbi, _ := filepath.EvalSymlinks(inputs.AbiPath) assert.Equal(t, expectedAbi, actualAbi) assert.Equal(t, "bindings", inputs.PkgName) - expectedOut, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "src", "generated")) - actualOut, _ := filepath.EvalSymlinks(inputs.OutPath) - assert.Equal(t, expectedOut, actualOut) + expectedGoOut, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "src", "generated")) + actualGoOut, _ := filepath.EvalSymlinks(inputs.GoOutPath) + assert.Equal(t, expectedGoOut, actualGoOut) + assert.Empty(t, inputs.TSOutPath) +} + +func TestResolveInputs_TypeScriptDefaults(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + contractsDir := filepath.Join(tempDir, "contracts") + err = os.MkdirAll(contractsDir, 0755) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + err = os.Chdir(tempDir) + require.NoError(t, err) + + runtimeCtx := &runtime.Context{} + handler := newHandler(runtimeCtx) + + v := viper.New() + v.Set("typescript", true) + v.Set("pkg", "bindings") + + inputs, err := handler.ResolveInputs([]string{"evm"}, v) + require.NoError(t, err) + + expectedRoot, _ := filepath.EvalSymlinks(tempDir) + actualRoot, _ := filepath.EvalSymlinks(inputs.ProjectRoot) + assert.Equal(t, expectedRoot, actualRoot) + assert.True(t, inputs.TypeScript) + + // ABI path: contracts/evm/src/abi + expectedAbi, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "src", "abi")) + actualAbi, _ := filepath.EvalSymlinks(inputs.AbiPath) + assert.Equal(t, expectedAbi, actualAbi) + + // TS output path: contracts/evm/ts/generated + expectedTSOut, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "ts", "generated")) + actualTSOut, _ := filepath.EvalSymlinks(inputs.TSOutPath) + assert.Equal(t, expectedTSOut, actualTSOut) + assert.Empty(t, inputs.GoOutPath) +} + +func TestAutoDetect_GoOnly(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + contractsDir := filepath.Join(tempDir, "contracts") + err = os.MkdirAll(contractsDir, 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tempDir, "main.go"), []byte("package main\nfunc main() {}"), 0600) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + _ = os.Chdir(tempDir) + + runtimeCtx := &runtime.Context{} + handler := newHandler(runtimeCtx) + + v := viper.New() + inputs, err := handler.ResolveInputs([]string{"evm"}, v) + require.NoError(t, err) + + assert.True(t, inputs.GoLang, "Go should be auto-detected") + assert.False(t, inputs.TypeScript, "TypeScript should not be detected") + assert.NotEmpty(t, inputs.GoOutPath) + assert.Empty(t, inputs.TSOutPath) +} + +func TestAutoDetect_TypeScriptOnly(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + contractsDir := filepath.Join(tempDir, "contracts") + err = os.MkdirAll(contractsDir, 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tempDir, "main.ts"), []byte("export function main() {}"), 0600) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + _ = os.Chdir(tempDir) + + runtimeCtx := &runtime.Context{} + handler := newHandler(runtimeCtx) + + v := viper.New() + inputs, err := handler.ResolveInputs([]string{"evm"}, v) + require.NoError(t, err) + + assert.False(t, inputs.GoLang, "Go should not be detected") + assert.True(t, inputs.TypeScript, "TypeScript should be auto-detected") + assert.Empty(t, inputs.GoOutPath) + assert.NotEmpty(t, inputs.TSOutPath) + expectedTSOut, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "ts", "generated")) + actualTSOut, _ := filepath.EvalSymlinks(inputs.TSOutPath) + assert.Equal(t, expectedTSOut, actualTSOut) +} + +func TestAutoDetect_Both(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + contractsDir := filepath.Join(tempDir, "contracts") + err = os.MkdirAll(contractsDir, 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tempDir, "main.go"), []byte("package main\nfunc main() {}"), 0600) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tempDir, "main.ts"), []byte("export function main() {}"), 0600) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + _ = os.Chdir(tempDir) + + runtimeCtx := &runtime.Context{} + handler := newHandler(runtimeCtx) + + v := viper.New() + inputs, err := handler.ResolveInputs([]string{"evm"}, v) + require.NoError(t, err) + + assert.True(t, inputs.GoLang, "Go should be auto-detected") + assert.True(t, inputs.TypeScript, "TypeScript should be auto-detected") + assert.NotEmpty(t, inputs.GoOutPath) + assert.NotEmpty(t, inputs.TSOutPath) +} + +func TestExplicitGoFlag(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + err = os.MkdirAll(filepath.Join(tempDir, "contracts"), 0755) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + _ = os.Chdir(tempDir) + + runtimeCtx := &runtime.Context{} + handler := newHandler(runtimeCtx) + + v := viper.New() + v.Set("go", true) + inputs, err := handler.ResolveInputs([]string{"evm"}, v) + require.NoError(t, err) + + assert.True(t, inputs.GoLang) + assert.False(t, inputs.TypeScript) + assert.NotEmpty(t, inputs.GoOutPath) + assert.Empty(t, inputs.TSOutPath) + expectedGoOut, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "src", "generated")) + actualGoOut, _ := filepath.EvalSymlinks(inputs.GoOutPath) + assert.Equal(t, expectedGoOut, actualGoOut) +} + +func TestExplicitTypeScriptFlag(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + err = os.MkdirAll(filepath.Join(tempDir, "contracts"), 0755) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + _ = os.Chdir(tempDir) + + runtimeCtx := &runtime.Context{} + handler := newHandler(runtimeCtx) + + v := viper.New() + v.Set("typescript", true) + inputs, err := handler.ResolveInputs([]string{"evm"}, v) + require.NoError(t, err) + + assert.False(t, inputs.GoLang) + assert.True(t, inputs.TypeScript) + assert.Empty(t, inputs.GoOutPath) + assert.NotEmpty(t, inputs.TSOutPath) + expectedTSOut, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "ts", "generated")) + actualTSOut, _ := filepath.EvalSymlinks(inputs.TSOutPath) + assert.Equal(t, expectedTSOut, actualTSOut) +} + +func TestBothFlagsExplicit(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + contractsDir := filepath.Join(tempDir, "contracts") + err = os.MkdirAll(contractsDir, 0755) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + _ = os.Chdir(tempDir) + + runtimeCtx := &runtime.Context{} + handler := newHandler(runtimeCtx) + + v := viper.New() + v.Set("go", true) + v.Set("typescript", true) + inputs, err := handler.ResolveInputs([]string{"evm"}, v) + require.NoError(t, err) + + assert.True(t, inputs.GoLang) + assert.True(t, inputs.TypeScript) + assert.NotEmpty(t, inputs.GoOutPath) + assert.NotEmpty(t, inputs.TSOutPath) +} + +func TestOutputPathsSeparation(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + contractsDir := filepath.Join(tempDir, "contracts") + err = os.MkdirAll(contractsDir, 0755) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + _ = os.Chdir(tempDir) + + runtimeCtx := &runtime.Context{} + handler := newHandler(runtimeCtx) + + v := viper.New() + v.Set("go", true) + v.Set("typescript", true) + inputs, err := handler.ResolveInputs([]string{"evm"}, v) + require.NoError(t, err) + + // Go path must contain src/generated + assert.Contains(t, inputs.GoOutPath, "src", "Go output path should contain src") + assert.Contains(t, inputs.GoOutPath, "generated", "Go output path should contain generated") + + // TS path must contain ts/generated + assert.Contains(t, inputs.TSOutPath, "ts", "TS output path should contain ts") + assert.Contains(t, inputs.TSOutPath, "generated", "TS output path should contain generated") + + // Paths must be different + assert.NotEqual(t, inputs.GoOutPath, inputs.TSOutPath, "Go and TS output paths must be different") +} + +func TestEndToEnd_TypeScriptGeneration(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + abiDir := filepath.Join(tempDir, "contracts", "evm", "src", "abi") + err = os.MkdirAll(abiDir, 0755) + require.NoError(t, err) + + abiContent := `[{"type":"function","name":"getValue","inputs":[],"outputs":[{"name":"","type":"uint256"}],"stateMutability":"view"}]` + err = os.WriteFile(filepath.Join(abiDir, "SimpleContract.abi"), []byte(abiContent), 0600) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + _ = os.Chdir(tempDir) + + logger := zerolog.New(os.Stderr).With().Timestamp().Logger() + runtimeCtx := &runtime.Context{Logger: &logger} + handler := newHandler(runtimeCtx) + + v := viper.New() + v.Set("typescript", true) + v.Set("pkg", "bindings") + inputs, err := handler.ResolveInputs([]string{"evm"}, v) + require.NoError(t, err) + require.NoError(t, handler.ValidateInputs(inputs)) + require.NoError(t, handler.Execute(inputs)) + + tsOutDir := filepath.Join(tempDir, "contracts", "evm", "ts", "generated") + require.FileExists(t, filepath.Join(tsOutDir, "SimpleContract.ts")) + require.FileExists(t, filepath.Join(tsOutDir, "SimpleContract_mock.ts")) + require.FileExists(t, filepath.Join(tsOutDir, "index.ts")) } // command should run in projectRoot which contains contracts directory @@ -108,8 +403,8 @@ func TestResolveInputs_CustomProjectRoot(t *testing.T) { // Test with custom project root v := viper.New() v.Set("project-root", tempDir) - v.Set("language", "go") // Default from StringP - v.Set("pkg", "bindings") // Default from StringP + v.Set("go", true) + v.Set("pkg", "bindings") _, err = handler.ResolveInputs([]string{"evm"}, v) require.Error(t, err) @@ -152,8 +447,8 @@ func TestResolveInputs_EmptyProjectRoot(t *testing.T) { // Test with empty project root (should use current directory) v := viper.New() v.Set("project-root", "") - v.Set("language", "go") // Default from StringP - v.Set("pkg", "bindings") // Default from StringP + v.Set("go", true) + v.Set("pkg", "bindings") inputs, err := handler.ResolveInputs([]string{"evm"}, v) require.NoError(t, err) @@ -163,14 +458,14 @@ func TestResolveInputs_EmptyProjectRoot(t *testing.T) { actualRoot, _ := filepath.EvalSymlinks(inputs.ProjectRoot) assert.Equal(t, expectedRoot, actualRoot) assert.Equal(t, "evm", inputs.ChainFamily) - assert.Equal(t, "go", inputs.Language) + assert.True(t, inputs.GoLang) expectedAbi, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "src", "abi")) actualAbi, _ := filepath.EvalSymlinks(inputs.AbiPath) assert.Equal(t, expectedAbi, actualAbi) assert.Equal(t, "bindings", inputs.PkgName) - expectedOut, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "src", "generated")) - actualOut, _ := filepath.EvalSymlinks(inputs.OutPath) - assert.Equal(t, expectedOut, actualOut) + expectedGoOut, _ := filepath.EvalSymlinks(filepath.Join(tempDir, "contracts", "evm", "src", "generated")) + actualGoOut, _ := filepath.EvalSymlinks(inputs.GoOutPath) + assert.Equal(t, expectedGoOut, actualGoOut) } func TestValidateInputs_RequiredChainFamily(t *testing.T) { @@ -181,10 +476,10 @@ func TestValidateInputs_RequiredChainFamily(t *testing.T) { inputs := Inputs{ ProjectRoot: "/tmp", ChainFamily: "", // Missing required field - Language: "go", + GoLang: true, AbiPath: "/tmp/abi", PkgName: "bindings", - OutPath: "/tmp/out", + GoOutPath: "/tmp/out", } err := handler.ValidateInputs(inputs) @@ -211,10 +506,10 @@ func TestValidateInputs_ValidInputs(t *testing.T) { inputs := Inputs{ ProjectRoot: tempDir, ChainFamily: "evm", - Language: "go", + GoLang: true, AbiPath: abiFile, PkgName: "bindings", - OutPath: tempDir, + GoOutPath: tempDir, } err = handler.ValidateInputs(inputs) @@ -232,6 +527,26 @@ func TestValidateInputs_ValidInputs(t *testing.T) { err = handler.ValidateInputs(inputs) require.NoError(t, err) assert.True(t, handler.validated) + + // Test validation with directory containing .abi files for TypeScript (unified extension) + abiDir2 := filepath.Join(tempDir, "abi_ts") + err = os.MkdirAll(abiDir2, 0755) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(abiDir2, "Contract.abi"), []byte(abiContent), 0600) + require.NoError(t, err) + + tsInputs := Inputs{ + ProjectRoot: tempDir, + ChainFamily: "evm", + TypeScript: true, + AbiPath: abiDir2, + PkgName: "bindings", + TSOutPath: tempDir, + } + handler2 := newHandler(runtimeCtx) + err = handler2.ValidateInputs(tsInputs) + require.NoError(t, err) + assert.True(t, handler2.validated) } func TestValidateInputs_InvalidChainFamily(t *testing.T) { @@ -247,10 +562,10 @@ func TestValidateInputs_InvalidChainFamily(t *testing.T) { inputs := Inputs{ ProjectRoot: tempDir, ChainFamily: "solana", // No longer supported - Language: "go", + GoLang: true, AbiPath: tempDir, PkgName: "bindings", - OutPath: tempDir, + GoOutPath: tempDir, } err = handler.ValidateInputs(inputs) @@ -258,28 +573,29 @@ func TestValidateInputs_InvalidChainFamily(t *testing.T) { assert.Contains(t, err.Error(), "chain-family") } -func TestValidateInputs_InvalidLanguage(t *testing.T) { - // Create a temporary directory for testing +func TestValidateInputs_NoLanguageSpecified(t *testing.T) { tempDir, err := os.MkdirTemp("", "generate-bindings-test") require.NoError(t, err) defer os.RemoveAll(tempDir) + // Create contracts dir but no .go or .ts files for auto-detect + contractsDir := filepath.Join(tempDir, "contracts") + err = os.MkdirAll(contractsDir, 0755) + require.NoError(t, err) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { _ = os.Chdir(originalDir) }() + _ = os.Chdir(tempDir) + runtimeCtx := &runtime.Context{} handler := newHandler(runtimeCtx) - // Test validation with invalid language - inputs := Inputs{ - ProjectRoot: tempDir, - ChainFamily: "evm", - Language: "typescript", // No longer supported - AbiPath: tempDir, - PkgName: "bindings", - OutPath: tempDir, - } - - err = handler.ValidateInputs(inputs) + // ResolveInputs should error when neither --go nor --typescript and nothing detected + v := viper.New() + _, err = handler.ResolveInputs([]string{"evm"}, v) require.Error(t, err) - assert.Contains(t, err.Error(), "language") + assert.Contains(t, err.Error(), "no target language") } func TestValidateInputs_NonExistentDirectory(t *testing.T) { @@ -290,10 +606,10 @@ func TestValidateInputs_NonExistentDirectory(t *testing.T) { inputs := Inputs{ ProjectRoot: "/non/existent/path", ChainFamily: "evm", - Language: "go", + GoLang: true, AbiPath: "/non/existent/abi", PkgName: "bindings", - OutPath: "/non/existent/out", + GoOutPath: "/non/existent/out", } err := handler.ValidateInputs(inputs) @@ -330,10 +646,10 @@ func TestProcessAbiDirectory_MultipleFiles(t *testing.T) { inputs := Inputs{ ProjectRoot: tempDir, ChainFamily: "evm", - Language: "go", + GoLang: true, AbiPath: abiDir, PkgName: "bindings", - OutPath: outDir, + GoOutPath: outDir, } // This test will fail because it tries to call the actual bindings.GenerateBindings @@ -392,10 +708,10 @@ func TestProcessAbiDirectory_CreatesPerContractDirectories(t *testing.T) { inputs := Inputs{ ProjectRoot: tempDir, ChainFamily: "evm", - Language: "go", + GoLang: true, AbiPath: abiDir, PkgName: "bindings", - OutPath: outDir, + GoOutPath: outDir, } // Try to process - the mock ABI content might actually work @@ -432,15 +748,43 @@ func TestProcessAbiDirectory_NoAbiFiles(t *testing.T) { inputs := Inputs{ ProjectRoot: tempDir, ChainFamily: "evm", - Language: "go", + GoLang: true, + AbiPath: abiDir, + PkgName: "bindings", + GoOutPath: outDir, + } + + err = handler.processAbiDirectory(inputs) + require.Error(t, err) + assert.Contains(t, err.Error(), "no *.abi files found") +} + +func TestProcessAbiDirectory_NoAbiFiles_TypeScript(t *testing.T) { + tempDir, err := os.MkdirTemp("", "generate-bindings-test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + abiDir := filepath.Join(tempDir, "abi") + outDir := filepath.Join(tempDir, "generated") + err = os.MkdirAll(abiDir, 0755) + require.NoError(t, err) + + logger := zerolog.New(os.Stderr).With().Timestamp().Logger() + runtimeCtx := &runtime.Context{Logger: &logger} + handler := newHandler(runtimeCtx) + + inputs := Inputs{ + ProjectRoot: tempDir, + ChainFamily: "evm", + TypeScript: true, AbiPath: abiDir, PkgName: "bindings", - OutPath: outDir, + TSOutPath: outDir, } err = handler.processAbiDirectory(inputs) require.Error(t, err) - assert.Contains(t, err.Error(), "no .abi files found") + assert.Contains(t, err.Error(), "no *.abi files found") } func TestProcessAbiDirectory_PackageNameCollision(t *testing.T) { @@ -472,10 +816,10 @@ func TestProcessAbiDirectory_PackageNameCollision(t *testing.T) { inputs := Inputs{ ProjectRoot: tempDir, ChainFamily: "evm", - Language: "go", + GoLang: true, AbiPath: abiDir, PkgName: "bindings", - OutPath: outDir, + GoOutPath: outDir, } err = handler.processAbiDirectory(inputs) @@ -494,16 +838,15 @@ func TestProcessAbiDirectory_NonExistentDirectory(t *testing.T) { inputs := Inputs{ ProjectRoot: "/tmp", ChainFamily: "evm", - Language: "go", + GoLang: true, AbiPath: "/non/existent/abi", PkgName: "bindings", - OutPath: "/tmp/out", + GoOutPath: "/tmp/out", } err := handler.processAbiDirectory(inputs) require.Error(t, err) - // For non-existent directory, filepath.Glob returns empty slice, so we get the "no .abi files found" error - assert.Contains(t, err.Error(), "no .abi files found") + assert.Contains(t, err.Error(), "no *.abi files found") } // TestGenerateBindings_UnconventionalNaming tests binding generation for contracts diff --git a/docs/cre_generate-bindings.md b/docs/cre_generate-bindings.md index 862f4109..2297984f 100644 --- a/docs/cre_generate-bindings.md +++ b/docs/cre_generate-bindings.md @@ -23,10 +23,11 @@ cre generate-bindings [optional flags] ``` -a, --abi string Path to ABI directory (defaults to contracts/{chain-family}/src/abi/) + --go Generate Go bindings -h, --help help for generate-bindings - -l, --language string Target language (go) (default "go") -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) + --typescript Generate TypeScript bindings ``` ### Options inherited from parent commands