diff --git a/cmd/common/compile.go b/cmd/common/compile.go index 764a9c51..6ee4164d 100644 --- a/cmd/common/compile.go +++ b/cmd/common/compile.go @@ -28,7 +28,16 @@ func getBuildCmd(workflowRootFolder, mainFile, language string, stripSymbols boo 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 d70b735a..56d42165 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -432,32 +432,33 @@ 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 account": {}, - "cre secrets": {}, - "cre templates": {}, - "cre templates list": {}, - "cre templates add": {}, - "cre templates remove": {}, - "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": {}, } _, exists := excludedCommands[cmd.CommandPath()] diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 771eef49..dc4e89fe 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -207,14 +207,41 @@ func (h *handler) ResolveInputs(v *viper.Viper, creSettings *settings.Settings) } if len(clients) == 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 workflow.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, + ) } 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 Inputs{}, 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)) + } + // Key not set or placeholder — require it for broadcast, otherwise use default for simulation if v.GetBool("broadcast") { return Inputs{}, 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("0000000000000000000000000000000000000000000000000000000000000001") if err != nil { @@ -1140,3 +1167,13 @@ func getEVMTriggerLogFromValues(ctx context.Context, ethClient *ethclient.Client } return pbLog, 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 +} diff --git a/cmd/workflow/simulate/simulator_utils.go b/cmd/workflow/simulate/simulator_utils.go index 6334a2c5..2f62e95e 100644 --- a/cmd/workflow/simulate/simulator_utils.go +++ b/cmd/workflow/simulate/simulator_utils.go @@ -6,6 +6,7 @@ import ( "fmt" "net/url" "regexp" + "sort" "strconv" "strings" "time" @@ -27,6 +28,21 @@ type ChainConfig struct { Forwarder string } +// SupportedChainNames returns the human-readable names of all supported EVM chains, +// sorted alphabetically. +func SupportedChainNames() []string { + var names []string + for _, chain := range SupportedEVM { + name, err := settings.GetChainNameByChainSelector(chain.Selector) + if err != nil { + continue + } + names = append(names, name) + } + sort.Strings(names) + return names +} + // SupportedEVM is the canonical list you can range over. var SupportedEVM = []ChainConfig{ // Ethereum @@ -185,7 +201,7 @@ func redactURL(rawURL string) string { // experimentalForwarders keys identify experimental chains (not in chain-selectors). func runRPCHealthCheck(clients map[uint64]*ethclient.Client, experimentalForwarders map[uint64]common.Address) error { if len(clients) == 0 { - return fmt.Errorf("check your settings: no RPC URLs found for supported or experimental chains") + return fmt.Errorf("no RPC URLs found for supported or experimental chains. Run 'cre workflow supported-chains' to see all supported chain names") } var errs []error diff --git a/cmd/workflow/simulate/utils_test.go b/cmd/workflow/simulate/utils_test.go index 14c5fd26..881d794c 100644 --- a/cmd/workflow/simulate/utils_test.go +++ b/cmd/workflow/simulate/utils_test.go @@ -151,7 +151,7 @@ func TestHealthCheck_NoClientsConfigured(t *testing.T) { if err == nil { t.Fatalf("expected error for no clients configured") } - mustContain(t, err.Error(), "check your settings: no RPC URLs found for supported or experimental chains") + mustContain(t, err.Error(), "no RPC URLs found for supported or experimental chains") } func TestHealthCheck_NilClient(t *testing.T) { diff --git a/cmd/workflow/workflow.go b/cmd/workflow/workflow.go index f03a7bdf..85aac4c9 100644 --- a/cmd/workflow/workflow.go +++ b/cmd/workflow/workflow.go @@ -1,6 +1,8 @@ package workflow import ( + "fmt" + "github.com/spf13/cobra" "github.com/smartcontractkit/cre-cli/cmd/workflow/activate" @@ -23,6 +25,21 @@ func New(runtimeContext *runtime.Context) *cobra.Command { Long: `The workflow command allows you to register and manage existing workflows.`, } + supportedChainsCmd := &cobra.Command{ + Use: "supported-chains", + Short: "List all supported chain names", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + names := simulate.SupportedChainNames() + fmt.Println("Supported chain names:") + for _, name := range names { + fmt.Printf(" %s\n", name) + } + return nil + }, + } + + workflowCmd.AddCommand(supportedChainsCmd) 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 0bdfb9a6..e151addd 100644 --- a/docs/cre_workflow.md +++ b/docs/cre_workflow.md @@ -38,4 +38,5 @@ cre workflow [optional flags] * [cre workflow limits](cre_workflow_limits.md) - Manage simulation limits * [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 be7b75e2..266f7c6c 100644 --- a/internal/ethkeys/keys.go +++ b/internal/ethkeys/keys.go @@ -10,7 +10,16 @@ import ( 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 ba5b30d2..0c1dd713 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 d96e27e8..7b4463d4 100644 --- a/internal/settings/settings_get.go +++ b/internal/settings/settings_get.go @@ -253,7 +253,7 @@ func GetChainNameByChainSelector(chainSelector uint64) (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 ed193351..af67993c 100644 --- a/internal/settings/settings_load.go +++ b/internal/settings/settings_load.go @@ -103,7 +103,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, + ) } }