Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions cmd/workflow/simulate/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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 {
Expand Down
43 changes: 11 additions & 32 deletions internal/settings/settings_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand Down
96 changes: 0 additions & 96 deletions internal/settings/settings_get_test.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
19 changes: 10 additions & 9 deletions internal/settings/settings_load.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 5 additions & 6 deletions internal/settings/template/project.yaml.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -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:<chain-selector>@1.0.0
# In your workflow, reference the chain as evm:ChainSelector:<chain-id>@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:
Expand Down
25 changes: 3 additions & 22 deletions internal/settings/workflow_settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading