diff --git a/.gitignore b/.gitignore index 521b4a59..75064238 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ encrypted.secrets.json # Output produced by e2e Anvil tests test/test.yaml + +.claude diff --git a/cmd/common/compile.go b/cmd/common/compile.go index cc7c8bf7..2456ec77 100644 --- a/cmd/common/compile.go +++ b/cmd/common/compile.go @@ -46,7 +46,16 @@ func getBuildCmd(workflowRootFolder, mainFile, language string, opts WorkflowCom return func() ([]byte, error) { out, err := cmd.CombinedOutput() if err != nil { - return nil, fmt.Errorf("%w\nbuild output:\n%s", err, strings.TrimSpace(string(out))) + outStr := strings.TrimSpace(string(out)) + if strings.Contains(outStr, "Script not found") && strings.Contains(outStr, "cre-compile") { + return nil, fmt.Errorf("TypeScript compilation failed: 'cre-compile' command not found.\n\n" + + "The 'cre-compile' tool is provided by the @chainlink/cre-sdk package.\n\n" + + "To fix:\n" + + " • Run 'bun install' in your project to install dependencies\n" + + " • Update your project dependencies with 'cre update '\n" + + " • If starting fresh, use 'cre workflow init' to scaffold a properly configured workflow") + } + return nil, fmt.Errorf("%w\nbuild output:\n%s", err, outStr) } b, err := os.ReadFile(tmpPath) _ = os.Remove(tmpPath) diff --git a/cmd/root.go b/cmd/root.go index fcdd5c84..b0a54e2e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -451,35 +451,35 @@ func newRootCommand() *cobra.Command { func isLoadSettings(cmd *cobra.Command) bool { // It is not expected to have the settings file when running the following commands var excludedCommands = map[string]struct{}{ - "cre version": {}, - "cre login": {}, - "cre logout": {}, - "cre whoami": {}, - "cre account access": {}, - "cre account list-key": {}, - "cre init": {}, - "cre generate-bindings": {}, - "cre completion bash": {}, - "cre completion fish": {}, - "cre completion powershell": {}, - "cre completion zsh": {}, - "cre help": {}, - "cre update": {}, - "cre workflow": {}, - "cre workflow custom-build": {}, - "cre workflow limits": {}, - "cre workflow limits export": {}, - "cre workflow build": {}, - "cre workflow list": {}, - "cre account": {}, - "cre secrets": {}, - "cre templates": {}, - "cre templates list": {}, - "cre templates add": {}, - "cre templates remove": {}, - "cre registry": {}, - "cre registry list": {}, - "cre": {}, + "cre version": {}, + "cre login": {}, + "cre logout": {}, + "cre whoami": {}, + "cre account access": {}, + "cre account list-key": {}, + "cre init": {}, + "cre generate-bindings": {}, + "cre completion bash": {}, + "cre completion fish": {}, + "cre completion powershell": {}, + "cre completion zsh": {}, + "cre help": {}, + "cre update": {}, + "cre workflow": {}, + "cre workflow supported-chains": {}, + "cre workflow custom-build": {}, + "cre workflow limits": {}, + "cre workflow limits export": {}, + "cre workflow build": {}, + "cre account": {}, + "cre secrets": {}, + "cre templates": {}, + "cre templates list": {}, + "cre templates add": {}, + "cre templates remove": {}, + "cre registry": {}, + "cre registry list": {}, + "cre": {}, } _, exists := excludedCommands[cmd.CommandPath()] diff --git a/cmd/workflow/simulate/chain/evm/chaintype.go b/cmd/workflow/simulate/chain/evm/chaintype.go index 76f8784b..5cece5ef 100644 --- a/cmd/workflow/simulate/chain/evm/chaintype.go +++ b/cmd/workflow/simulate/chain/evm/chaintype.go @@ -223,9 +223,24 @@ func (ct *EVMChainType) RunHealthCheck(resolved chain.ResolvedChains) error { func (ct *EVMChainType) ResolveKey(creSettings *settings.Settings, broadcast bool) (interface{}, error) { pk, err := crypto.HexToECDSA(creSettings.User.EthPrivateKey) if err != nil { + // If the user explicitly set a key that looks like a hex string but is + // malformed (wrong length, invalid chars), always error with guidance. + // Skip placeholder values like "your-eth-private-key" from the default .env template. + if creSettings.User.EthPrivateKey != "" && isHexString(creSettings.User.EthPrivateKey) { + return nil, fmt.Errorf( + "invalid private key: expected 64 hex characters (256 bits), got %d characters.\n\n"+ + "The CLI reads CRE_ETH_PRIVATE_KEY from your .env file or system environment.\n"+ + "The 0x prefix is supported and stripped automatically.\n\n"+ + "Common issues:\n"+ + " • Pasted an Ethereum address (40 chars) instead of a private key (64 chars)\n"+ + " • Value has extra quotes — use CRE_ETH_PRIVATE_KEY=abc123... without wrapping quotes\n"+ + " • Key was truncated during copy-paste", + len(creSettings.User.EthPrivateKey)) + } if broadcast { return nil, fmt.Errorf( - "failed to parse private key, required to broadcast. Please check CRE_ETH_PRIVATE_KEY in your .env file or system environment: %w", err) + "a private key is required for --broadcast mode.\n" + + "Set CRE_ETH_PRIVATE_KEY in your .env file or system environment") } pk, err = crypto.HexToECDSA(defaultSentinelPrivateKey) if err != nil { @@ -239,6 +254,16 @@ func (ct *EVMChainType) ResolveKey(creSettings *settings.Settings, broadcast boo return pk, nil } +// isHexString returns true if s contains only hexadecimal characters (0-9, a-f, A-F). +func isHexString(s string) bool { + for _, c := range s { + if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') { + return false + } + } + return len(s) > 0 +} + // CLI input keys consumed from chain.TriggerParams.ChainTypeInputs. const ( TriggerInputTxHash = "evm-tx-hash" diff --git a/cmd/workflow/simulate/chain/evm/chaintype_test.go b/cmd/workflow/simulate/chain/evm/chaintype_test.go index 976e94b6..b02b267c 100644 --- a/cmd/workflow/simulate/chain/evm/chaintype_test.go +++ b/cmd/workflow/simulate/chain/evm/chaintype_test.go @@ -118,18 +118,18 @@ func TestEVMChainType_ResolveKey(t *testing.T) { checkD1: true, }, { - name: "too-short key, non-broadcast, falls back + warns", - pk: "ab", - broadcast: false, - wantStderr: "Using default private key", - checkD1: true, + name: "too-short but valid-hex key, non-broadcast, invalid-length hard error", + pk: "ab", + broadcast: false, + wantErr: true, + errContains: "invalid private key: expected 64 hex characters", }, { name: "invalid hex, broadcast, hard error", pk: "notahex", broadcast: true, wantErr: true, - errContains: "failed to parse private key, required to broadcast", + errContains: "a private key is required for --broadcast mode", }, { name: "empty key, broadcast, hard error", @@ -156,7 +156,7 @@ func TestEVMChainType_ResolveKey(t *testing.T) { pk: "ab", broadcast: true, wantErr: true, - errContains: "required to broadcast", + errContains: "invalid private key: expected 64 hex characters", }, } diff --git a/cmd/workflow/simulate/chain/evm/supported_chains.go b/cmd/workflow/simulate/chain/evm/supported_chains.go index 7db9aeed..96bef7ff 100644 --- a/cmd/workflow/simulate/chain/evm/supported_chains.go +++ b/cmd/workflow/simulate/chain/evm/supported_chains.go @@ -1,11 +1,29 @@ package evm import ( + "sort" + chainselectors "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain" + "github.com/smartcontractkit/cre-cli/internal/settings" ) +// SupportedChainNames returns the human-readable names of all supported EVM chains, +// sorted alphabetically. +func SupportedChainNames() []string { + var names []string + for _, c := range SupportedChains { + name, err := settings.GetChainNameByChainSelector(c.Selector) + if err != nil { + continue + } + names = append(names, name) + } + sort.Strings(names) + return names +} + // SupportedChains is the canonical list of EVM chains supported for simulation. var SupportedChains = []chain.ChainConfig{ // Ethereum diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 7fba1870..e7e20b8a 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -155,7 +155,18 @@ func (h *handler) ResolveInputs(v *viper.Viper, creSettings *settings.Settings) totalClients += len(fc) } if totalClients == 0 { - return Inputs{}, fmt.Errorf("no RPC URLs found for supported or experimental chains") + target, _ := settings.GetTarget(v) + if target == "" { + target = "(none)" + } + return Inputs{}, fmt.Errorf( + "no RPC URLs found for target %q\n\n"+ + "To fix:\n"+ + " • Check that your project.yaml has an 'rpcs' section under the target %q\n"+ + " • Ensure chain names are valid (run 'cre workflow supported-chains' to see all supported names)\n"+ + " • Verify the correct target is selected via --target or CRE_TARGET", + target, target, + ) } broadcast := v.GetBool("broadcast") diff --git a/cmd/workflow/supported_chains/supported_chains.go b/cmd/workflow/supported_chains/supported_chains.go new file mode 100644 index 00000000..7a09a58e --- /dev/null +++ b/cmd/workflow/supported_chains/supported_chains.go @@ -0,0 +1,25 @@ +package supported_chains + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate/chain/evm" +) + +func New() *cobra.Command { + return &cobra.Command{ + Use: "supported-chains", + Short: "List all supported chain names", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + names := evm.SupportedChainNames() + fmt.Println("Supported chain names:") + for _, name := range names { + fmt.Printf(" %s\n", name) + } + return nil + }, + } +} diff --git a/cmd/workflow/workflow.go b/cmd/workflow/workflow.go index cc39b70c..04ca0e75 100644 --- a/cmd/workflow/workflow.go +++ b/cmd/workflow/workflow.go @@ -13,6 +13,7 @@ import ( workflowlist "github.com/smartcontractkit/cre-cli/cmd/workflow/list" "github.com/smartcontractkit/cre-cli/cmd/workflow/pause" "github.com/smartcontractkit/cre-cli/cmd/workflow/simulate" + supported_chains "github.com/smartcontractkit/cre-cli/cmd/workflow/supported_chains" "github.com/smartcontractkit/cre-cli/cmd/workflow/test" "github.com/smartcontractkit/cre-cli/internal/runtime" ) @@ -24,6 +25,7 @@ func New(runtimeContext *runtime.Context) *cobra.Command { Long: `The workflow command allows you to register and manage existing workflows.`, } + workflowCmd.AddCommand(supported_chains.New()) workflowCmd.AddCommand(activate.New(runtimeContext)) workflowCmd.AddCommand(build.New(runtimeContext)) workflowCmd.AddCommand(convert.New(runtimeContext)) diff --git a/docs/cre_workflow.md b/docs/cre_workflow.md index 00e56839..b4704bab 100644 --- a/docs/cre_workflow.md +++ b/docs/cre_workflow.md @@ -39,4 +39,5 @@ cre workflow [optional flags] * [cre workflow list](cre_workflow_list.md) - Lists workflows deployed for your organization * [cre workflow pause](cre_workflow_pause.md) - Pauses workflow on the Workflow Registry contract * [cre workflow simulate](cre_workflow_simulate.md) - Simulates a workflow +* [cre workflow supported-chains](cre_workflow_supported-chains.md) - List all supported chain names diff --git a/docs/cre_workflow_supported-chains.md b/docs/cre_workflow_supported-chains.md new file mode 100644 index 00000000..7f8406ae --- /dev/null +++ b/docs/cre_workflow_supported-chains.md @@ -0,0 +1,28 @@ +## cre workflow supported-chains + +List all supported chain names + +``` +cre workflow supported-chains [optional flags] +``` + +### Options + +``` + -h, --help help for supported-chains +``` + +### Options inherited from parent commands + +``` + -e, --env string Path to .env file which contains sensitive info + -R, --project-root string Path to the project root + -E, --public-env string Path to .env.public file which contains shared, non-sensitive build config + -T, --target string Use target settings from YAML config + -v, --verbose Run command in VERBOSE mode +``` + +### SEE ALSO + +* [cre workflow](cre_workflow.md) - Manages workflows + diff --git a/internal/context/project_context.go b/internal/context/project_context.go index 88e79f24..416fa371 100644 --- a/internal/context/project_context.go +++ b/internal/context/project_context.go @@ -77,7 +77,14 @@ func SetProjectContext(projectPath string) error { } if !found { - return fmt.Errorf("no project settings file found in current directory or parent directories") + return fmt.Errorf( + "no CRE project found (could not locate '%s' in '%s' or any parent directory)\n\n"+ + "To fix:\n"+ + " • Run this command from inside a CRE project directory\n"+ + " • Or run 'cre init' to create a new project here\n"+ + " • Or use '--%s ' to specify the project location", + constants.DefaultProjectSettingsFileName, cwd, "project-root", + ) } // Get the directory containing the project settings file (this is the project root) diff --git a/internal/context/project_context_test.go b/internal/context/project_context_test.go index 9382f80e..b4edac62 100644 --- a/internal/context/project_context_test.go +++ b/internal/context/project_context_test.go @@ -151,7 +151,7 @@ func TestSetProjectContext(t *testing.T) { }, projectPath: "", // Empty path should trigger search expectError: true, - errorContains: "no project settings file found", + errorContains: "no CRE project found", }, { name: "fails when project path doesn't exist", diff --git a/internal/ethkeys/keys.go b/internal/ethkeys/keys.go index 9d462a8f..a19461f4 100644 --- a/internal/ethkeys/keys.go +++ b/internal/ethkeys/keys.go @@ -29,7 +29,16 @@ func FormatWorkflowOwnerAddress(s string) (string, error) { func DeriveEthAddressFromPrivateKey(privateKeyHex string) (string, error) { privateKey, err := crypto.HexToECDSA(privateKeyHex) if err != nil { - return "", fmt.Errorf("failed to parse private key. Please check CRE_ETH_PRIVATE_KEY in your .env file or system environment: %w", err) + return "", fmt.Errorf( + "invalid private key: expected 64 hex characters (256 bits), got %d characters.\n\n"+ + "The CLI reads CRE_ETH_PRIVATE_KEY from your .env file or system environment.\n"+ + "The 0x prefix is supported and stripped automatically.\n\n"+ + "Common issues:\n"+ + " • Pasted an Ethereum address (40 chars) instead of a private key (64 chars)\n"+ + " • Value has extra quotes — use CRE_ETH_PRIVATE_KEY=abc123... without wrapping quotes\n"+ + " • Key was truncated during copy-paste", + len(privateKeyHex), + ) } publicKey := privateKey.Public() diff --git a/internal/ethkeys/keys_test.go b/internal/ethkeys/keys_test.go index f1fae9d1..5762ea12 100644 --- a/internal/ethkeys/keys_test.go +++ b/internal/ethkeys/keys_test.go @@ -68,7 +68,7 @@ func TestDeriveEthAddressFromPrivateKey_InvalidInput(t *testing.T) { t.Fatalf("expected error, got nil (addr=%q)", addr) } - if !strings.Contains(strings.ToLower(err.Error()), "failed to parse private key") { + if !strings.Contains(strings.ToLower(err.Error()), "invalid private key") { t.Fatalf("unexpected error message: %v", err) } }) diff --git a/internal/settings/settings_get.go b/internal/settings/settings_get.go index 3c778a51..9ddb2d28 100644 --- a/internal/settings/settings_get.go +++ b/internal/settings/settings_get.go @@ -265,7 +265,7 @@ func ChainNameFromSelectorString(raw string) (string, error) { func GetChainSelectorByChainName(name string) (uint64, error) { chainID, err := chainSelectors.ChainIdFromName(name) if err != nil { - return 0, fmt.Errorf("failed to get chain ID from name %q: %w", name, err) + return 0, fmt.Errorf("failed to get chain ID from name %q: %w\n Run 'cre workflow supported-chains' to see all valid chain names", name, err) } selector, err := chainSelectors.SelectorFromChainId(chainID) diff --git a/internal/settings/settings_load.go b/internal/settings/settings_load.go index 61710f9c..6a9e394c 100644 --- a/internal/settings/settings_load.go +++ b/internal/settings/settings_load.go @@ -104,7 +104,15 @@ func LoadSettingsIntoViper(v *viper.Viper, cmd *cobra.Command) error { if context.IsWorkflowCommand(cmd) { // Step 2: Load workflow settings next (overwrites values from project settings) if err := mergeConfigToViper(v, constants.DefaultWorkflowSettingsFileName); err != nil { - return fmt.Errorf("failed to load workflow settings: %w", err) + cwd, _ := os.Getwd() + return fmt.Errorf( + "workflow settings file not found: no '%s' in '%s'\n\n"+ + "To fix:\n"+ + " • Run 'cre workflow init' to create a properly initialized workflow\n"+ + " • If this workflow was manually created, add a %s with your target configuration\n"+ + " • Check that the workflow folder path argument is correct", + constants.DefaultWorkflowSettingsFileName, cwd, constants.DefaultWorkflowSettingsFileName, + ) } }