diff --git a/cmd/workflow/simulate/simulate.go b/cmd/workflow/simulate/simulate.go index 31f8223e..bd62e471 100644 --- a/cmd/workflow/simulate/simulate.go +++ b/cmd/workflow/simulate/simulate.go @@ -146,22 +146,22 @@ func (h *handler) ResolveInputs(v *viper.Viper, creSettings *settings.Settings) for _, ec := range expChains { // Validate required fields - if ec.Selector == 0 { - return Inputs{}, fmt.Errorf("experimental chain missing chain-selector") + if ec.ChainID == 0 { + return Inputs{}, fmt.Errorf("experimental chain missing chain-id") } if strings.TrimSpace(ec.RPCURL) == "" { - return Inputs{}, fmt.Errorf("experimental chain %d missing rpc-url", ec.Selector) + return Inputs{}, fmt.Errorf("experimental chain %d missing rpc-url", ec.ChainID) } if strings.TrimSpace(ec.Forwarder) == "" { - return Inputs{}, fmt.Errorf("experimental chain %d missing forwarder", ec.Selector) + return Inputs{}, fmt.Errorf("experimental chain %d missing forwarder", ec.ChainID) } - // Check if selector already exists (supported chain) - if _, exists := clients[ec.Selector]; exists { + // Check if chain ID already exists (supported chain) + if _, exists := clients[ec.ChainID]; exists { // Find the supported chain's forwarder var supportedForwarder string for _, supported := range SupportedEVM { - if supported.Selector == ec.Selector { + if supported.Selector == ec.ChainID { supportedForwarder = supported.Forwarder break } @@ -170,27 +170,27 @@ func (h *handler) ResolveInputs(v *viper.Viper, creSettings *settings.Settings) expFwd := common.HexToAddress(ec.Forwarder) if supportedForwarder != "" && common.HexToAddress(supportedForwarder) == expFwd { // Same forwarder, just debug log - h.log.Debug().Uint64("selector", ec.Selector).Msg("Experimental chain matches supported chain config") + h.log.Debug().Uint64("chain-id", ec.ChainID).Msg("Experimental chain matches supported chain config") continue } // Different forwarder - respect user's config, warn about override - fmt.Printf("Warning: experimental chain %d overrides supported chain forwarder (supported: %s, experimental: %s)\n", ec.Selector, supportedForwarder, ec.Forwarder) + fmt.Printf("Warning: experimental chain %d overrides supported chain forwarder (supported: %s, experimental: %s)\n", ec.ChainID, supportedForwarder, ec.Forwarder) // Use existing client but override the forwarder - experimentalForwarders[ec.Selector] = expFwd + experimentalForwarders[ec.ChainID] = expFwd continue } // Dial the RPC c, err := ethclient.Dial(ec.RPCURL) if err != nil { - return Inputs{}, fmt.Errorf("failed to create eth client for experimental chain %d: %w", ec.Selector, err) + return Inputs{}, fmt.Errorf("failed to create eth client for experimental chain %d: %w", ec.ChainID, err) } - clients[ec.Selector] = c - experimentalForwarders[ec.Selector] = common.HexToAddress(ec.Forwarder) - fmt.Printf("Added experimental chain (selector: %d)\n", ec.Selector) + clients[ec.ChainID] = c + experimentalForwarders[ec.ChainID] = common.HexToAddress(ec.Forwarder) + fmt.Printf("Added experimental chain (chain-id: %d)\n", ec.ChainID) } if len(clients) == 0 { diff --git a/internal/settings/settings_get.go b/internal/settings/settings_get.go index b02c021e..cd191f0b 100644 --- a/internal/settings/settings_get.go +++ b/internal/settings/settings_get.go @@ -34,20 +34,15 @@ type RpcEndpoint struct { // "private URL" can be feeded to the settings file by specifying the env var name where the real URL is kept, e.g. // url_private: RPC_URL_ETH_SEPOLIA Url string `mapstructure:"url" yaml:"url"` - // ChainSelector identifies experimental chains (not in official chain-selectors). - // When set (non-zero), the entry is treated as an experimental chain and Forwarder is required. - ChainSelector uint64 `mapstructure:"chain-selector" yaml:"chain-selector"` - // Forwarder is the forwarder contract address. Required when ChainSelector is set. - Forwarder string `mapstructure:"forwarder" yaml:"forwarder"` } // ExperimentalChain represents an EVM chain not in official chain-selectors. -// Automatically derived from rpcs entries that have chain-selector set. -// The Selector is used as the key for EVM clients and forwarders. +// Automatically used by the simulator when present in the target's experimental-chains config. +// The ChainID is used as the selector key for EVM clients and forwarders. type ExperimentalChain struct { - Selector uint64 // derived from RpcEndpoint.ChainSelector - RPCURL string // derived from RpcEndpoint.Url - Forwarder string // derived from RpcEndpoint.Forwarder + ChainID uint64 `mapstructure:"chain-id" yaml:"chain-id"` + RPCURL string `mapstructure:"rpc-url" yaml:"rpc-url"` + Forwarder string `mapstructure:"forwarder" yaml:"forwarder"` } func GetRpcUrlSettings(v *viper.Viper, chainName string) (string, error) { @@ -72,39 +67,23 @@ func GetRpcUrlSettings(v *viper.Viper, chainName string) (string, error) { return "", fmt.Errorf("rpc url not found for chain %s", chainName) } -// GetExperimentalChains derives experimental chains from rpcs entries that have chain-selector set. -// An entry with chain-selector != 0 is treated as an experimental chain and requires forwarder to be set. -// Returns an empty slice if no experimental chains are configured. +// GetExperimentalChains reads the experimental-chains list from the current target. +// Returns an empty slice if the key is not set or unmarshalling fails. func GetExperimentalChains(v *viper.Viper) ([]ExperimentalChain, error) { target, err := GetTarget(v) if err != nil { return nil, err } - keyWithTarget := fmt.Sprintf("%s.%s", target, RpcsSettingName) + keyWithTarget := fmt.Sprintf("%s.%s", target, ExperimentalChainsSettingName) if !v.IsSet(keyWithTarget) { return nil, nil } - var rpcs []RpcEndpoint - err = v.UnmarshalKey(keyWithTarget, &rpcs) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal rpcs: %w", err) - } - var chains []ExperimentalChain - for _, rpc := range rpcs { - if rpc.ChainSelector == 0 { - continue // not an experimental chain - } - if strings.TrimSpace(rpc.Forwarder) == "" { - return nil, fmt.Errorf("experimental chain (chain-selector %d) requires forwarder", rpc.ChainSelector) - } - chains = append(chains, ExperimentalChain{ - Selector: rpc.ChainSelector, - RPCURL: rpc.Url, - Forwarder: rpc.Forwarder, - }) + err = v.UnmarshalKey(keyWithTarget, &chains) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal experimental-chains: %w", err) } return chains, nil diff --git a/internal/settings/settings_get_test.go b/internal/settings/settings_get_test.go index 5922057c..9d421ba6 100644 --- a/internal/settings/settings_get_test.go +++ b/internal/settings/settings_get_test.go @@ -1,111 +1,15 @@ package settings_test import ( - "fmt" "testing" "github.com/spf13/viper" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/smartcontractkit/cre-cli/internal/constants" "github.com/smartcontractkit/cre-cli/internal/settings" ) -func TestGetExperimentalChains(t *testing.T) { - t.Run("returns experimental chains from rpcs with chain-selector set", func(t *testing.T) { - v := viper.New() - v.Set(settings.CreTargetEnvVar, "staging") - v.Set(fmt.Sprintf("staging.%s", settings.RpcsSettingName), []map[string]interface{}{ - { - "chain-name": "ethereum-testnet-sepolia", - "url": "https://sepolia.rpc.org", - "chain-selector": 0, // not experimental - }, - { - "chain-name": "worldchain-sepolia", - "url": "https://worldchain-sepolia.rpc.org", - "chain-selector": uint64(5299555114858065850), - "forwarder": "0x76c9cf548b4179F8901cda1f8623568b58215E62", - }, - }) - - chains, err := settings.GetExperimentalChains(v) - require.NoError(t, err) - require.Len(t, chains, 1) - assert.Equal(t, uint64(5299555114858065850), chains[0].Selector) - assert.Equal(t, "https://worldchain-sepolia.rpc.org", chains[0].RPCURL) - assert.Equal(t, "0x76c9cf548b4179F8901cda1f8623568b58215E62", chains[0].Forwarder) - }) - - t.Run("returns error when chain-selector is set but forwarder is empty", func(t *testing.T) { - v := viper.New() - v.Set(settings.CreTargetEnvVar, "staging") - v.Set(fmt.Sprintf("staging.%s", settings.RpcsSettingName), []map[string]interface{}{ - { - "chain-name": "worldchain-sepolia", - "url": "https://worldchain-sepolia.rpc.org", - "chain-selector": uint64(5299555114858065850), - "forwarder": "", // empty forwarder should cause error - }, - }) - - chains, err := settings.GetExperimentalChains(v) - assert.Error(t, err) - assert.Nil(t, chains) - assert.Contains(t, err.Error(), "requires forwarder") - }) - - t.Run("returns empty slice when no experimental chains configured", func(t *testing.T) { - v := viper.New() - v.Set(settings.CreTargetEnvVar, "staging") - v.Set(fmt.Sprintf("staging.%s", settings.RpcsSettingName), []map[string]interface{}{ - { - "chain-name": "ethereum-testnet-sepolia", - "url": "https://sepolia.rpc.org", - }, - }) - - chains, err := settings.GetExperimentalChains(v) - require.NoError(t, err) - assert.Empty(t, chains) - }) - - t.Run("returns nil when rpcs key is not set", func(t *testing.T) { - v := viper.New() - v.Set(settings.CreTargetEnvVar, "staging") - - chains, err := settings.GetExperimentalChains(v) - require.NoError(t, err) - assert.Nil(t, chains) - }) - - t.Run("handles multiple experimental chains", func(t *testing.T) { - v := viper.New() - v.Set(settings.CreTargetEnvVar, "staging") - v.Set(fmt.Sprintf("staging.%s", settings.RpcsSettingName), []map[string]interface{}{ - { - "chain-name": "experimental-chain-1", - "url": "https://chain1.rpc.org", - "chain-selector": uint64(1111111111), - "forwarder": "0x1111111111111111111111111111111111111111", - }, - { - "chain-name": "experimental-chain-2", - "url": "https://chain2.rpc.org", - "chain-selector": uint64(2222222222), - "forwarder": "0x2222222222222222222222222222222222222222", - }, - }) - - chains, err := settings.GetExperimentalChains(v) - require.NoError(t, err) - require.Len(t, chains, 2) - assert.Equal(t, uint64(1111111111), chains[0].Selector) - assert.Equal(t, uint64(2222222222), chains[1].Selector) - }) -} - func TestGetWorkflowOwner(t *testing.T) { validPrivKey := "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" expectedOwner := "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" diff --git a/internal/settings/settings_load.go b/internal/settings/settings_load.go index 7e56aec0..5b8d2990 100644 --- a/internal/settings/settings_load.go +++ b/internal/settings/settings_load.go @@ -13,15 +13,16 @@ import ( // Config names (YAML field paths) const ( - WorkflowOwnerSettingName = "account.workflow-owner-address" - WorkflowNameSettingName = "user-workflow.workflow-name" - WorkflowPathSettingName = "workflow-artifacts.workflow-path" - ConfigPathSettingName = "workflow-artifacts.config-path" - SecretsPathSettingName = "workflow-artifacts.secrets-path" - SethConfigPathSettingName = "logging.seth-config-path" - RegistriesSettingName = "contracts.registries" - KeystoneSettingName = "contracts.keystone" - RpcsSettingName = "rpcs" + WorkflowOwnerSettingName = "account.workflow-owner-address" + WorkflowNameSettingName = "user-workflow.workflow-name" + WorkflowPathSettingName = "workflow-artifacts.workflow-path" + ConfigPathSettingName = "workflow-artifacts.config-path" + SecretsPathSettingName = "workflow-artifacts.secrets-path" + SethConfigPathSettingName = "logging.seth-config-path" + RegistriesSettingName = "contracts.registries" + KeystoneSettingName = "contracts.keystone" + RpcsSettingName = "rpcs" + ExperimentalChainsSettingName = "experimental-chains" // used by simulator when present in target config ) type Flag struct { diff --git a/internal/settings/template/project.yaml.tpl b/internal/settings/template/project.yaml.tpl index 7aaca5fe..85e7db28 100644 --- a/internal/settings/template/project.yaml.tpl +++ b/internal/settings/template/project.yaml.tpl @@ -16,13 +16,12 @@ # # Experimental chains (automatically used by the simulator when present): # Use this for chains not yet in official chain-selectors (e.g., hackathons, new chain integrations). -# Add chain-selector and forwarder to an rpcs entry to mark it as experimental. -# In your workflow, reference the chain as evm:ChainSelector:@1.0.0 +# In your workflow, reference the chain as evm:ChainSelector:@1.0.0 # -# - chain-name: my-experimental-chain # Optional label for the chain -# chain-selector: 5299555114858065850 # Chain selector (required for experimental) -# url: "https://rpc.example.com" # RPC endpoint URL -# forwarder: "0x..." # Forwarder contract address (required when chain-selector is set) +# experimental-chains: +# - chain-id: 12345 # The numeric chain ID +# rpc-url: "https://rpc.example.com" # RPC endpoint URL +# forwarder: "0x..." # Forwarder contract address on the chain # ========================================================================== staging-settings: diff --git a/internal/settings/workflow_settings.go b/internal/settings/workflow_settings.go index aaca7ae1..cf62e3d9 100644 --- a/internal/settings/workflow_settings.go +++ b/internal/settings/workflow_settings.go @@ -137,30 +137,11 @@ func flattenWorkflowSettingsToViper(v *viper.Viper, target string) error { func validateSettings(config *WorkflowSettings) error { // TODO validate that all chain names mentioned for the contracts above have a matching URL specified for _, rpc := range config.RPCs { - // Determine a label for error messages - var label string - if rpc.ChainSelector != 0 { - label = fmt.Sprintf("chain-selector %d", rpc.ChainSelector) - } else { - label = rpc.ChainName - } - - // Always validate the URL if err := isValidRpcUrl(rpc.Url); err != nil { - return errors.Wrap(err, "invalid rpc url for "+label) + return errors.Wrap(err, "invalid rpc url for "+rpc.ChainName) } - - if rpc.ChainSelector != 0 { - // Experimental chain: require forwarder, skip chain-name validation - if strings.TrimSpace(rpc.Forwarder) == "" { - return fmt.Errorf("experimental chain (chain-selector %d) requires forwarder", rpc.ChainSelector) - } - // Skip chain-name validation for experimental chains (may not be in chain-selectors registry) - } else { - // Standard chain: validate chain-name as before - if err := IsValidChainName(rpc.ChainName); err != nil { - return err - } + if err := IsValidChainName(rpc.ChainName); err != nil { + return err } } return nil diff --git a/internal/settings/workflow_settings_test.go b/internal/settings/workflow_settings_test.go deleted file mode 100644 index a6018b22..00000000 --- a/internal/settings/workflow_settings_test.go +++ /dev/null @@ -1,149 +0,0 @@ -package settings - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestValidateSettings_ExperimentalChains(t *testing.T) { - t.Run("passes validation for experimental chain with chain-selector and forwarder", func(t *testing.T) { - config := &WorkflowSettings{ - RPCs: []RpcEndpoint{ - { - ChainName: "experimental-chain", - Url: "https://rpc.example.com", - ChainSelector: 5299555114858065850, - Forwarder: "0x76c9cf548b4179F8901cda1f8623568b58215E62", - }, - }, - } - - err := validateSettings(config) - require.NoError(t, err) - }) - - t.Run("fails validation for experimental chain with chain-selector but no forwarder", func(t *testing.T) { - config := &WorkflowSettings{ - RPCs: []RpcEndpoint{ - { - ChainName: "experimental-chain", - Url: "https://rpc.example.com", - ChainSelector: 5299555114858065850, - Forwarder: "", // missing forwarder - }, - }, - } - - err := validateSettings(config) - require.Error(t, err) - assert.Contains(t, err.Error(), "requires forwarder") - assert.Contains(t, err.Error(), "5299555114858065850") - }) - - t.Run("fails validation for experimental chain with whitespace-only forwarder", func(t *testing.T) { - config := &WorkflowSettings{ - RPCs: []RpcEndpoint{ - { - ChainName: "experimental-chain", - Url: "https://rpc.example.com", - ChainSelector: 5299555114858065850, - Forwarder: " ", // whitespace-only forwarder - }, - }, - } - - err := validateSettings(config) - require.Error(t, err) - assert.Contains(t, err.Error(), "requires forwarder") - }) - - t.Run("skips chain-name validation for experimental chains", func(t *testing.T) { - // An experimental chain with an invalid chain name should still pass validation - // because chain-name validation is skipped for experimental chains - config := &WorkflowSettings{ - RPCs: []RpcEndpoint{ - { - ChainName: "not-a-real-chain-name", // invalid chain name - Url: "https://rpc.example.com", - ChainSelector: 5299555114858065850, // marks as experimental - Forwarder: "0x76c9cf548b4179F8901cda1f8623568b58215E62", - }, - }, - } - - err := validateSettings(config) - require.NoError(t, err) // should pass because chain-name validation is skipped - }) - - t.Run("validates chain-name for non-experimental chains", func(t *testing.T) { - config := &WorkflowSettings{ - RPCs: []RpcEndpoint{ - { - ChainName: "invalid-chain-name", // invalid chain name - Url: "https://rpc.example.com", - ChainSelector: 0, // not experimental - }, - }, - } - - err := validateSettings(config) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid chain name") - }) - - t.Run("validates URL for experimental chains", func(t *testing.T) { - config := &WorkflowSettings{ - RPCs: []RpcEndpoint{ - { - ChainName: "experimental-chain", - Url: "not-a-valid-url", - ChainSelector: 5299555114858065850, - Forwarder: "0x76c9cf548b4179F8901cda1f8623568b58215E62", - }, - }, - } - - err := validateSettings(config) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid rpc url") - }) - - t.Run("validates URL for non-experimental chains", func(t *testing.T) { - config := &WorkflowSettings{ - RPCs: []RpcEndpoint{ - { - ChainName: "ethereum-testnet-sepolia", - Url: "ftp://invalid-scheme.com", // invalid scheme - ChainSelector: 0, - }, - }, - } - - err := validateSettings(config) - require.Error(t, err) - assert.Contains(t, err.Error(), "invalid scheme") - }) - - t.Run("handles mixed experimental and non-experimental chains", func(t *testing.T) { - config := &WorkflowSettings{ - RPCs: []RpcEndpoint{ - { - ChainName: "ethereum-testnet-sepolia", - Url: "https://sepolia.rpc.org", - // ChainSelector is 0 (non-experimental) - }, - { - ChainName: "worldchain-sepolia", - Url: "https://worldchain.rpc.org", - ChainSelector: 5299555114858065850, - Forwarder: "0x76c9cf548b4179F8901cda1f8623568b58215E62", - }, - }, - } - - err := validateSettings(config) - require.NoError(t, err) - }) -}