diff --git a/cmd/root.go b/cmd/root.go index c506beb3..a411aef7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -262,6 +262,10 @@ func newRootCommand() *cobra.Command { return fmt.Errorf("%w", err) } + if err := runtimeContext.AttachResolvedRegistry(); err != nil { + return err + } + // Restart spinner for remaining initialization if showSpinner { spinner = ui.NewSpinner() diff --git a/cmd/workflow/activate/activate.go b/cmd/workflow/activate/activate.go index 0eb759da..5c3ddd74 100644 --- a/cmd/workflow/activate/activate.go +++ b/cmd/workflow/activate/activate.go @@ -45,6 +45,12 @@ func New(runtimeContext *runtime.Context) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { handler := newHandler(runtimeContext) + if runtimeContext.ResolvedRegistry != nil { + if err := runtimeContext.ResolvedRegistry.RequireOnChainRegistry("activate"); err != nil { + return err + } + } + inputs, err := handler.ResolveInputs(runtimeContext.Viper) if err != nil { return err @@ -108,9 +114,9 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { return Inputs{ WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, - DonFamily: h.environmentSet.DonFamily, - WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, - WorkflowRegistryContractChainName: h.environmentSet.WorkflowRegistryChainName, + DonFamily: h.runtimeContext.ResolvedRegistry.DonFamily, + WorkflowRegistryContractAddress: h.runtimeContext.ResolvedRegistry.Address, + WorkflowRegistryContractChainName: h.runtimeContext.ResolvedRegistry.ChainName, }, nil } @@ -182,10 +188,10 @@ func (h *handler) Execute() error { switch txOut.Type { case client.Regular: ui.Success(fmt.Sprintf("Transaction confirmed: %s", txOut.Hash)) - ui.URL(fmt.Sprintf("%s/tx/%s", h.environmentSet.WorkflowRegistryChainExplorerURL, txOut.Hash)) + ui.URL(fmt.Sprintf("%s/tx/%s", h.runtimeContext.ResolvedRegistry.ExplorerURL, txOut.Hash)) ui.Line() ui.Success("Workflow activated successfully") - ui.Dim(fmt.Sprintf(" Contract address: %s", h.environmentSet.WorkflowRegistryAddress)) + ui.Dim(fmt.Sprintf(" Contract address: %s", h.runtimeContext.ResolvedRegistry.Address)) ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) ui.Dim(fmt.Sprintf(" Workflow ID: %s", hex.EncodeToString(latest.WorkflowId[:]))) @@ -207,9 +213,9 @@ func (h *handler) Execute() error { ui.Line() case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(h.environmentSet.WorkflowRegistryChainName) + chainSelector, err := settings.GetChainSelectorByChainName(h.runtimeContext.ResolvedRegistry.ChainName) if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", h.environmentSet.WorkflowRegistryChainName, err) + return fmt.Errorf("failed to get chain selector for chain %q: %w", h.runtimeContext.ResolvedRegistry.ChainName, err) } mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) if err != nil { diff --git a/cmd/workflow/delete/delete.go b/cmd/workflow/delete/delete.go index 78ee36e8..d36b61fa 100644 --- a/cmd/workflow/delete/delete.go +++ b/cmd/workflow/delete/delete.go @@ -44,6 +44,12 @@ func New(runtimeContext *runtime.Context) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { handler := newHandler(runtimeContext, cmd.InOrStdin()) + if runtimeContext.ResolvedRegistry != nil { + if err := runtimeContext.ResolvedRegistry.RequireOnChainRegistry("delete"); err != nil { + return err + } + } + inputs, err := handler.ResolveInputs(runtimeContext.Viper) if err != nil { return err @@ -114,8 +120,8 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), - WorkflowRegistryContractChainName: h.environmentSet.WorkflowRegistryChainName, - WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, + WorkflowRegistryContractChainName: h.runtimeContext.ResolvedRegistry.ChainName, + WorkflowRegistryContractAddress: h.runtimeContext.ResolvedRegistry.Address, }, nil } @@ -193,7 +199,7 @@ func (h *handler) Execute() error { switch txOut.Type { case client.Regular: ui.Success("Transaction confirmed") - ui.URL(fmt.Sprintf("%s/tx/%s", h.environmentSet.WorkflowRegistryChainExplorerURL, txOut.Hash)) + ui.URL(fmt.Sprintf("%s/tx/%s", h.runtimeContext.ResolvedRegistry.ExplorerURL, txOut.Hash)) ui.Success(fmt.Sprintf("Deleted workflow ID: %s", hex.EncodeToString(wf.WorkflowId[:]))) case client.Raw: @@ -212,9 +218,9 @@ func (h *handler) Execute() error { ui.Line() case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(h.environmentSet.WorkflowRegistryChainName) + chainSelector, err := settings.GetChainSelectorByChainName(h.runtimeContext.ResolvedRegistry.ChainName) if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", h.environmentSet.WorkflowRegistryChainName, err) + return fmt.Errorf("failed to get chain selector for chain %q: %w", h.runtimeContext.ResolvedRegistry.ChainName, err) } mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) if err != nil { diff --git a/cmd/workflow/deploy/artifacts.go b/cmd/workflow/deploy/artifacts.go index 7e85e185..db4aaf10 100644 --- a/cmd/workflow/deploy/artifacts.go +++ b/cmd/workflow/deploy/artifacts.go @@ -33,12 +33,12 @@ func (h *handler) uploadArtifacts() error { gql := graphqlclient.New(h.credentials, h.environmentSet, h.log) - chainSelector, err := settings.GetChainSelectorByChainName(h.environmentSet.WorkflowRegistryChainName) + chainSelector, err := settings.GetChainSelectorByChainName(h.runtimeContext.ResolvedRegistry.ChainName) if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", h.environmentSet.WorkflowRegistryChainName, err) + return fmt.Errorf("failed to get chain selector for chain %q: %w", h.runtimeContext.ResolvedRegistry.ChainName, err) } - storageClient := storageclient.New(gql, h.environmentSet.WorkflowRegistryAddress, h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, chainSelector, h.log) + storageClient := storageclient.New(gql, h.runtimeContext.ResolvedRegistry.Address, h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, chainSelector, h.log) if h.settings.StorageSettings.CREStorage.ServiceTimeout != 0 { storageClient.SetServiceTimeout(h.settings.StorageSettings.CREStorage.ServiceTimeout) } diff --git a/cmd/workflow/deploy/compile_test.go b/cmd/workflow/deploy/compile_test.go index aaa87ac3..33c5ce83 100644 --- a/cmd/workflow/deploy/compile_test.go +++ b/cmd/workflow/deploy/compile_test.go @@ -238,6 +238,7 @@ func createTestSettings(workflowOwnerAddress, workflowOwnerType, workflowName, w WorkflowOwnerAddress string `mapstructure:"workflow-owner-address" yaml:"workflow-owner-address"` WorkflowOwnerType string `mapstructure:"workflow-owner-type" yaml:"workflow-owner-type"` WorkflowName string `mapstructure:"workflow-name" yaml:"workflow-name"` + DeploymentRegistry string `mapstructure:"deployment-registry" yaml:"deployment-registry"` }{ WorkflowOwnerAddress: workflowOwnerAddress, WorkflowOwnerType: workflowOwnerType, diff --git a/cmd/workflow/deploy/deploy.go b/cmd/workflow/deploy/deploy.go index 843da5d4..8c5df691 100644 --- a/cmd/workflow/deploy/deploy.go +++ b/cmd/workflow/deploy/deploy.go @@ -95,6 +95,12 @@ func New(runtimeContext *runtime.Context) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { h := newHandler(runtimeContext, cmd.InOrStdin()) + if runtimeContext.ResolvedRegistry != nil { + if err := runtimeContext.ResolvedRegistry.RequireOnChainRegistry("deploy"); err != nil { + return err + } + } + inputs, err := h.ResolveInputs(runtimeContext.Viper) if err != nil { return err @@ -173,7 +179,7 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, WorkflowTag: workflowTag, ConfigURL: configURL, - DonFamily: h.environmentSet.DonFamily, + DonFamily: h.runtimeContext.ResolvedRegistry.DonFamily, WorkflowPath: h.settings.Workflow.WorkflowArtifactSettings.WorkflowPath, KeepAlive: false, @@ -182,8 +188,8 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { OutputPath: v.GetString("output"), WasmPath: v.GetString("wasm"), - WorkflowRegistryContractChainName: h.environmentSet.WorkflowRegistryChainName, - WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, + WorkflowRegistryContractChainName: h.runtimeContext.ResolvedRegistry.ChainName, + WorkflowRegistryContractAddress: h.runtimeContext.ResolvedRegistry.Address, OwnerLabel: v.GetString("owner-label"), SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name), SkipTypeChecks: v.GetBool(cmdcommon.SkipTypeChecksCLIFlag), diff --git a/cmd/workflow/deploy/register.go b/cmd/workflow/deploy/register.go index 4042c9db..e6a57be3 100644 --- a/cmd/workflow/deploy/register.go +++ b/cmd/workflow/deploy/register.go @@ -68,12 +68,12 @@ func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters) error switch txOut.Type { case client.Regular: ui.Success("Transaction confirmed") - ui.URL(fmt.Sprintf("%s/tx/%s", h.environmentSet.WorkflowRegistryChainExplorerURL, txOut.Hash)) + ui.URL(fmt.Sprintf("%s/tx/%s", h.runtimeContext.ResolvedRegistry.ExplorerURL, txOut.Hash)) ui.Line() ui.Success("Workflow deployed successfully") ui.Line() ui.Bold("Details:") - ui.Dim(fmt.Sprintf(" Contract address: %s", h.environmentSet.WorkflowRegistryAddress)) + ui.Dim(fmt.Sprintf(" Contract address: %s", h.runtimeContext.ResolvedRegistry.Address)) ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) ui.Dim(fmt.Sprintf(" Workflow ID: %s", h.workflowArtifact.WorkflowID)) @@ -99,9 +99,9 @@ func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters) error ui.Line() case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(h.environmentSet.WorkflowRegistryChainName) + chainSelector, err := settings.GetChainSelectorByChainName(h.runtimeContext.ResolvedRegistry.ChainName) if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", h.environmentSet.WorkflowRegistryChainName, err) + return fmt.Errorf("failed to get chain selector for chain %q: %w", h.runtimeContext.ResolvedRegistry.ChainName, err) } mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) if err != nil { diff --git a/cmd/workflow/pause/pause.go b/cmd/workflow/pause/pause.go index a1564764..00204dcd 100644 --- a/cmd/workflow/pause/pause.go +++ b/cmd/workflow/pause/pause.go @@ -45,6 +45,12 @@ func New(runtimeContext *runtime.Context) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { handler := newHandler(runtimeContext) + if runtimeContext.ResolvedRegistry != nil { + if err := runtimeContext.ResolvedRegistry.RequireOnChainRegistry("pause"); err != nil { + return err + } + } + inputs, err := handler.ResolveInputs(runtimeContext.Viper) if err != nil { return err @@ -107,8 +113,8 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) { return Inputs{ WorkflowName: h.settings.Workflow.UserWorkflowSettings.WorkflowName, WorkflowOwner: h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress, - WorkflowRegistryContractChainName: h.environmentSet.WorkflowRegistryChainName, - WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress, + WorkflowRegistryContractChainName: h.runtimeContext.ResolvedRegistry.ChainName, + WorkflowRegistryContractAddress: h.runtimeContext.ResolvedRegistry.Address, }, nil } @@ -176,11 +182,11 @@ func (h *handler) Execute() error { switch txOut.Type { case client.Regular: ui.Success("Transaction confirmed") - ui.URL(fmt.Sprintf("%s/tx/%s", h.environmentSet.WorkflowRegistryChainExplorerURL, txOut.Hash)) + ui.URL(fmt.Sprintf("%s/tx/%s", h.runtimeContext.ResolvedRegistry.ExplorerURL, txOut.Hash)) ui.Success("Workflows paused successfully") ui.Line() ui.Bold("Details:") - ui.Dim(fmt.Sprintf(" Contract address: %s", h.environmentSet.WorkflowRegistryAddress)) + ui.Dim(fmt.Sprintf(" Contract address: %s", h.runtimeContext.ResolvedRegistry.Address)) ui.Dim(fmt.Sprintf(" Transaction hash: %s", txOut.Hash)) ui.Dim(fmt.Sprintf(" Workflow Name: %s", workflowName)) for _, w := range activeWorkflowIDs { @@ -204,9 +210,9 @@ func (h *handler) Execute() error { ui.Line() case client.Changeset: - chainSelector, err := settings.GetChainSelectorByChainName(h.environmentSet.WorkflowRegistryChainName) + chainSelector, err := settings.GetChainSelectorByChainName(h.runtimeContext.ResolvedRegistry.ChainName) if err != nil { - return fmt.Errorf("failed to get chain selector for chain %q: %w", h.environmentSet.WorkflowRegistryChainName, err) + return fmt.Errorf("failed to get chain selector for chain %q: %w", h.runtimeContext.ResolvedRegistry.ChainName, err) } mcmsConfig, err := settings.GetMCMSConfig(h.settings, chainSelector) if err != nil { diff --git a/cmd/workflow/simulate/simulate_test.go b/cmd/workflow/simulate/simulate_test.go index b29d3f52..d5635077 100644 --- a/cmd/workflow/simulate/simulate_test.go +++ b/cmd/workflow/simulate/simulate_test.go @@ -111,6 +111,7 @@ func createSimulateTestSettings(workflowName, workflowPath, configPath string) * WorkflowOwnerAddress string `mapstructure:"workflow-owner-address" yaml:"workflow-owner-address"` WorkflowOwnerType string `mapstructure:"workflow-owner-type" yaml:"workflow-owner-type"` WorkflowName string `mapstructure:"workflow-name" yaml:"workflow-name"` + DeploymentRegistry string `mapstructure:"deployment-registry" yaml:"deployment-registry"` }{ WorkflowName: workflowName, }, diff --git a/internal/runtime/runtime_context.go b/internal/runtime/runtime_context.go index af367838..0450fe85 100644 --- a/internal/runtime/runtime_context.go +++ b/internal/runtime/runtime_context.go @@ -23,14 +23,15 @@ var ( ) type Context struct { - Logger *zerolog.Logger - Viper *viper.Viper - ClientFactory client.Factory - Settings *settings.Settings - Credentials *credentials.Credentials - EnvironmentSet *environments.EnvironmentSet - TenantContext *tenantctx.EnvironmentContext - Workflow WorkflowRuntime + Logger *zerolog.Logger + Viper *viper.Viper + ClientFactory client.Factory + Settings *settings.Settings + Credentials *credentials.Credentials + EnvironmentSet *environments.EnvironmentSet + TenantContext *tenantctx.EnvironmentContext + ResolvedRegistry *settings.ResolvedRegistry + Workflow WorkflowRuntime } type WorkflowRuntime struct { @@ -111,6 +112,24 @@ func (ctx *Context) AttachTenantContext(validationCtx context.Context) error { return nil } +// AttachResolvedRegistry resolves the deployment-registry from workflow +// settings against the tenant context registries. Must be called after +// AttachSettings and AttachTenantContext. +func (ctx *Context) AttachResolvedRegistry() error { + deploymentRegistry := "" + if ctx.Settings != nil { + deploymentRegistry = ctx.Settings.Workflow.UserWorkflowSettings.DeploymentRegistry + } + + resolved, err := settings.ResolveRegistry(deploymentRegistry, ctx.TenantContext, ctx.EnvironmentSet) + if err != nil { + return fmt.Errorf("failed to resolve deployment registry: %w", err) + } + + ctx.ResolvedRegistry = resolved + return nil +} + func (ctx *Context) AttachEnvironmentSet() error { var err error diff --git a/internal/settings/registry_resolution.go b/internal/settings/registry_resolution.go new file mode 100644 index 00000000..5020447f --- /dev/null +++ b/internal/settings/registry_resolution.go @@ -0,0 +1,128 @@ +package settings + +import ( + "fmt" + "strconv" + "strings" + + "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" +) + +// ResolvedRegistry holds the fully resolved registry configuration for a +// workflow command. It is built from either the static EnvironmentSet defaults +// or from a tenant context registry entry selected via deployment-registry. +type ResolvedRegistry struct { + ID string + Type string // "on-chain" or "off-chain" + Address string + ChainName string + DonFamily string + ExplorerURL string +} + +// ResolveRegistry maps an optional deployment-registry value to a concrete +// ResolvedRegistry. When deploymentRegistry is empty the static EnvironmentSet +// values are used (backwards-compatible default). When set, it is looked up in +// tenantCtx.Registries and the matching entry is used. Off-chain registries +// are rejected in production environments. +func ResolveRegistry( + deploymentRegistry string, + tenantCtx *tenantctx.EnvironmentContext, + envSet *environments.EnvironmentSet, +) (*ResolvedRegistry, error) { + if deploymentRegistry == "" { + return defaultFromEnvironmentSet(envSet), nil + } + + if tenantCtx == nil { + return nil, fmt.Errorf("deployment-registry %q is set but user context is not available — run `cre login` and retry", deploymentRegistry) + } + + reg := findRegistry(tenantCtx.Registries, deploymentRegistry) + if reg == nil { + return nil, fmt.Errorf("registry %q not found in context.yaml; available: [%s]", + deploymentRegistry, availableIDs(tenantCtx.Registries)) + } + + if reg.Type == "off-chain" { + if isProduction(envSet) { + return nil, fmt.Errorf("off-chain (private) registries are not yet supported in production") + } + return &ResolvedRegistry{ + ID: reg.ID, + Type: reg.Type, + DonFamily: tenantCtx.DefaultDonFamily, + }, nil + } + + resolved := &ResolvedRegistry{ + ID: reg.ID, + Type: reg.Type, + DonFamily: tenantCtx.DefaultDonFamily, + ExplorerURL: envSet.WorkflowRegistryChainExplorerURL, + } + + if reg.Address != nil { + resolved.Address = *reg.Address + } + + if reg.ChainSelector != nil { + sel, err := strconv.ParseUint(*reg.ChainSelector, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid chain_selector %q for registry %q: %w", *reg.ChainSelector, reg.ID, err) + } + name, err := GetChainNameByChainSelector(sel) + if err != nil { + return nil, fmt.Errorf("cannot resolve chain name for selector %d (registry %q): %w", sel, reg.ID, err) + } + resolved.ChainName = name + } + + return resolved, nil +} + +// TODO: remove this once off-chain routing is implemented +// RequireOnChainRegistry returns an error if the resolved registry is not +// on-chain. Use this in commands that only support on-chain workflow registries +// (deploy, pause, activate, delete) until off-chain routing is implemented. +func (r *ResolvedRegistry) RequireOnChainRegistry(commandName string) error { + if r.Type != "on-chain" { + return fmt.Errorf( + "%s currently only supports on-chain registries; deployment-registry %q is %s", + commandName, r.ID, r.Type, + ) + } + return nil +} + +func defaultFromEnvironmentSet(envSet *environments.EnvironmentSet) *ResolvedRegistry { + return &ResolvedRegistry{ + Type: "on-chain", + Address: envSet.WorkflowRegistryAddress, + ChainName: envSet.WorkflowRegistryChainName, + DonFamily: envSet.DonFamily, + ExplorerURL: envSet.WorkflowRegistryChainExplorerURL, + } +} + +func findRegistry(registries []*tenantctx.Registry, id string) *tenantctx.Registry { + for _, r := range registries { + if r.ID == id { + return r + } + } + return nil +} + +func availableIDs(registries []*tenantctx.Registry) string { + ids := make([]string, 0, len(registries)) + for _, r := range registries { + ids = append(ids, r.ID) + } + return strings.Join(ids, ", ") +} + +func isProduction(envSet *environments.EnvironmentSet) bool { + return envSet.EnvName == "" || envSet.EnvName == environments.DefaultEnv +} diff --git a/internal/settings/registry_resolution_test.go b/internal/settings/registry_resolution_test.go new file mode 100644 index 00000000..2ca37c9b --- /dev/null +++ b/internal/settings/registry_resolution_test.go @@ -0,0 +1,169 @@ +package settings + +import ( + "strings" + "testing" + + "github.com/smartcontractkit/cre-cli/internal/environments" + "github.com/smartcontractkit/cre-cli/internal/tenantctx" +) + +func strPtr(s string) *string { return &s } + +func stagingEnvSet() *environments.EnvironmentSet { + return &environments.EnvironmentSet{ + EnvName: "STAGING", + WorkflowRegistryAddress: "0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135", + WorkflowRegistryChainName: "ethereum-testnet-sepolia", + WorkflowRegistryChainExplorerURL: "https://sepolia.etherscan.io", + DonFamily: "zone-a", + } +} + +func prodEnvSet() *environments.EnvironmentSet { + return &environments.EnvironmentSet{ + EnvName: "PRODUCTION", + WorkflowRegistryAddress: "0x4Ac54353FA4Fa961AfcC5ec4B118596d3305E7e5", + WorkflowRegistryChainName: "ethereum-mainnet", + WorkflowRegistryChainExplorerURL: "https://etherscan.io", + DonFamily: "zone-a", + } +} + +func sampleTenantCtx() *tenantctx.EnvironmentContext { + return &tenantctx.EnvironmentContext{ + DefaultDonFamily: "zone-a", + Registries: []*tenantctx.Registry{ + { + ID: "onchain:ethereum-testnet-sepolia", + Label: "ethereum-testnet-sepolia (0xaE55...1135)", + Type: "on-chain", + ChainSelector: strPtr("16015286601757825753"), + Address: strPtr("0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135"), + }, + { + ID: "private", + Label: "Private (Chainlink-hosted)", + Type: "off-chain", + }, + }, + } +} + +func TestResolveRegistry_EmptyFallsBackToEnvSet(t *testing.T) { + envSet := stagingEnvSet() + resolved, err := ResolveRegistry("", nil, envSet) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resolved.Type != "on-chain" { + t.Errorf("expected on-chain, got %s", resolved.Type) + } + if resolved.Address != envSet.WorkflowRegistryAddress { + t.Errorf("expected address %s, got %s", envSet.WorkflowRegistryAddress, resolved.Address) + } + if resolved.ChainName != envSet.WorkflowRegistryChainName { + t.Errorf("expected chain %s, got %s", envSet.WorkflowRegistryChainName, resolved.ChainName) + } + if resolved.DonFamily != envSet.DonFamily { + t.Errorf("expected don %s, got %s", envSet.DonFamily, resolved.DonFamily) + } + if resolved.ExplorerURL != envSet.WorkflowRegistryChainExplorerURL { + t.Errorf("expected explorer %s, got %s", envSet.WorkflowRegistryChainExplorerURL, resolved.ExplorerURL) + } +} + +func TestResolveRegistry_OnChainFromContext(t *testing.T) { + resolved, err := ResolveRegistry("onchain:ethereum-testnet-sepolia", sampleTenantCtx(), stagingEnvSet()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resolved.Type != "on-chain" { + t.Errorf("expected on-chain, got %s", resolved.Type) + } + if resolved.Address != "0xaE55eB3EDAc48a1163EE2cbb1205bE1e90Ea1135" { + t.Errorf("unexpected address: %s", resolved.Address) + } + if resolved.ChainName != "ethereum-testnet-sepolia" { + t.Errorf("unexpected chain name: %s", resolved.ChainName) + } + if resolved.DonFamily != "zone-a" { + t.Errorf("unexpected don family: %s", resolved.DonFamily) + } +} + +func TestResolveRegistry_OffChainFromContext(t *testing.T) { + resolved, err := ResolveRegistry("private", sampleTenantCtx(), stagingEnvSet()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resolved.Type != "off-chain" { + t.Errorf("expected off-chain, got %s", resolved.Type) + } + if resolved.Address != "" { + t.Errorf("expected empty address for off-chain, got %s", resolved.Address) + } + if resolved.ChainName != "" { + t.Errorf("expected empty chain for off-chain, got %s", resolved.ChainName) + } + if resolved.DonFamily != "zone-a" { + t.Errorf("unexpected don family: %s", resolved.DonFamily) + } +} + +func TestResolveRegistry_UnknownID(t *testing.T) { + _, err := ResolveRegistry("does-not-exist", sampleTenantCtx(), stagingEnvSet()) + if err == nil { + t.Fatal("expected error for unknown registry ID") + } + if !strings.Contains(err.Error(), "not found in context.yaml") { + t.Errorf("unexpected error: %v", err) + } + if !strings.Contains(err.Error(), "onchain:ethereum-testnet-sepolia") { + t.Errorf("error should list available IDs: %v", err) + } +} + +func TestResolveRegistry_NilTenantContextWithID(t *testing.T) { + _, err := ResolveRegistry("private", nil, stagingEnvSet()) + if err == nil { + t.Fatal("expected error when TenantContext is nil with a registry ID set") + } + if !strings.Contains(err.Error(), "user context is not available") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestResolveRegistry_OffChainBlockedInProduction(t *testing.T) { + _, err := ResolveRegistry("private", sampleTenantCtx(), prodEnvSet()) + if err == nil { + t.Fatal("expected error for off-chain in production") + } + if !strings.Contains(err.Error(), "not yet supported in production") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestResolveRegistry_OffChainBlockedWhenEnvEmpty(t *testing.T) { + envSet := stagingEnvSet() + envSet.EnvName = "" + _, err := ResolveRegistry("private", sampleTenantCtx(), envSet) + if err == nil { + t.Fatal("expected error for off-chain when env name is empty (defaults to production)") + } + if !strings.Contains(err.Error(), "not yet supported in production") { + t.Errorf("unexpected error: %v", err) + } +} + +func TestRequireOnChainRegistry(t *testing.T) { + onChain := &ResolvedRegistry{ID: "onchain:ethereum-testnet-sepolia", Type: "on-chain"} + if err := onChain.RequireOnChainRegistry("deploy"); err != nil { + t.Errorf("on-chain should pass: %v", err) + } + + offChain := &ResolvedRegistry{ID: "private", Type: "off-chain"} + if err := offChain.RequireOnChainRegistry("deploy"); err == nil { + t.Error("off-chain should be rejected") + } +} diff --git a/internal/settings/settings_load.go b/internal/settings/settings_load.go index ed193351..61710f9c 100644 --- a/internal/settings/settings_load.go +++ b/internal/settings/settings_load.go @@ -21,6 +21,7 @@ const ( SethConfigPathSettingName = "logging.seth-config-path" RegistriesSettingName = "contracts.registries" KeystoneSettingName = "contracts.keystone" + DeploymentRegistrySettingName = "user-workflow.deployment-registry" RpcsSettingName = "rpcs" ExperimentalChainsSettingName = "experimental-chains" // used by simulator when present in target config ) diff --git a/internal/settings/workflow_settings.go b/internal/settings/workflow_settings.go index fd43c4bf..43488ae6 100644 --- a/internal/settings/workflow_settings.go +++ b/internal/settings/workflow_settings.go @@ -84,6 +84,7 @@ type WorkflowSettings struct { WorkflowOwnerAddress string `mapstructure:"workflow-owner-address" yaml:"workflow-owner-address"` WorkflowOwnerType string `mapstructure:"workflow-owner-type" yaml:"workflow-owner-type"` WorkflowName string `mapstructure:"workflow-name" yaml:"workflow-name"` + DeploymentRegistry string `mapstructure:"deployment-registry" yaml:"deployment-registry"` } `mapstructure:"user-workflow" yaml:"user-workflow"` WorkflowArtifactSettings struct { WorkflowPath string `mapstructure:"workflow-path" yaml:"workflow-path"` @@ -128,6 +129,7 @@ func loadWorkflowSettings(logger *zerolog.Logger, v *viper.Viper, cmd *cobra.Com } workflowSettings.UserWorkflowSettings.WorkflowName = getSetting(WorkflowNameSettingName) + workflowSettings.UserWorkflowSettings.DeploymentRegistry = getSetting(DeploymentRegistrySettingName) workflowSettings.WorkflowArtifactSettings.WorkflowPath = getSetting(WorkflowPathSettingName) workflowSettings.WorkflowArtifactSettings.ConfigPath = getSetting(ConfigPathSettingName) workflowSettings.WorkflowArtifactSettings.SecretsPath = getSetting(SecretsPathSettingName) diff --git a/internal/testutil/chainsim/simulated_environment.go b/internal/testutil/chainsim/simulated_environment.go index 4b2d012c..8e82163e 100644 --- a/internal/testutil/chainsim/simulated_environment.go +++ b/internal/testutil/chainsim/simulated_environment.go @@ -12,8 +12,9 @@ import ( "github.com/smartcontractkit/cre-cli/internal/credentials" "github.com/smartcontractkit/cre-cli/internal/environments" "github.com/smartcontractkit/cre-cli/internal/runtime" - "github.com/smartcontractkit/cre-cli/internal/settings" + settingspkg "github.com/smartcontractkit/cre-cli/internal/settings" "github.com/smartcontractkit/cre-cli/internal/testutil" + "github.com/smartcontractkit/cre-cli/internal/testutil/testsettings" ) type SimulatedEnvironment struct { @@ -61,8 +62,8 @@ func (se *SimulatedEnvironment) Close() { func (se *SimulatedEnvironment) createContextWithLogger(logger *zerolog.Logger) *runtime.Context { v := viper.New() - v.Set(settings.EthPrivateKeyEnvVar, TestPrivateKey) - settings, err := testutil.NewTestSettings(v, logger) + v.Set(settingspkg.EthPrivateKeyEnvVar, TestPrivateKey) + settings, err := testsettings.NewTestSettings(v, logger) if err != nil { logger.Warn().Err(err).Msg("failed to create new test settings") } @@ -79,13 +80,16 @@ func (se *SimulatedEnvironment) createContextWithLogger(logger *zerolog.Logger) logger.Warn().Err(err).Msg("failed to create new credentials") } + resolved, _ := settingspkg.ResolveRegistry("", nil, environmentSet) + ctx := &runtime.Context{ - Logger: logger, - Viper: v, - ClientFactory: simulatedFactory, - Settings: settings, - EnvironmentSet: environmentSet, - Credentials: creds, + Logger: logger, + Viper: v, + ClientFactory: simulatedFactory, + Settings: settings, + EnvironmentSet: environmentSet, + Credentials: creds, + ResolvedRegistry: resolved, } // Mark credentials as validated for tests to bypass validation diff --git a/internal/testutil/test_settings.go b/internal/testutil/testsettings/test_settings.go similarity index 98% rename from internal/testutil/test_settings.go rename to internal/testutil/testsettings/test_settings.go index cd3dcc43..d3f4148c 100644 --- a/internal/testutil/test_settings.go +++ b/internal/testutil/testsettings/test_settings.go @@ -1,4 +1,4 @@ -package testutil +package testsettings import ( _ "embed" diff --git a/internal/testutil/testdata/test-project.yaml b/internal/testutil/testsettings/testdata/test-project.yaml similarity index 100% rename from internal/testutil/testdata/test-project.yaml rename to internal/testutil/testsettings/testdata/test-project.yaml diff --git a/internal/testutil/testdata/test-workflow.yaml b/internal/testutil/testsettings/testdata/test-workflow.yaml similarity index 100% rename from internal/testutil/testdata/test-workflow.yaml rename to internal/testutil/testsettings/testdata/test-workflow.yaml