diff --git a/deployment/cre/workflow_registry/v2/changeset/configure_workflow_registry.go b/deployment/cre/workflow_registry/v2/changeset/configure_workflow_registry.go index 637b74f3678..c2e72979850 100644 --- a/deployment/cre/workflow_registry/v2/changeset/configure_workflow_registry.go +++ b/deployment/cre/workflow_registry/v2/changeset/configure_workflow_registry.go @@ -23,8 +23,78 @@ var _ cldf.ChangeSetV2[UpdateAllowedSignersInput] = UpdateAllowedSigners{} var _ cldf.ChangeSetV2[SetWorkflowOwnerConfigInput] = SetWorkflowOwnerConfig{} var _ cldf.ChangeSetV2[SetDONLimitInput] = SetDONLimit{} var _ cldf.ChangeSetV2[SetUserDONOverrideInput] = SetUserDONOverride{} +var _ cldf.ChangeSetV2[BatchSetUserDONOverrideInput] = BatchSetUserDONOverride{} var _ cldf.ChangeSetV2[SetCapabilitiesRegistryInput] = SetCapabilitiesRegistry{} +// prepareWorkflowRegistryDeps loads MCMS state, EVM chain, registry contract, and the transaction +// strategy needed to execute a workflow-registry operation. The returned deps is ready to pass to +// operations.ExecuteOperation and the strategy is reachable via deps.Strategy for the subsequent +// BuildProposal call in finalizeWorkflowRegistryOutput. +// +// Extracted to remove the ~30-line setup boilerplate that was previously duplicated across each +// Apply function in this file. New code should prefer this helper; existing Apply functions can +// be migrated incrementally. +func prepareWorkflowRegistryDeps( + e cldf.Environment, + chainSelector uint64, + qualifier string, + mcmsConfig *crecontracts.MCMSConfig, + description string, +) (contracts.WorkflowRegistryOpDeps, error) { + var mcmsContracts *evmstate.MCMSWithTimelockState + if mcmsConfig != nil { + loaded, err := strategies.GetMCMSContracts(e, chainSelector, *mcmsConfig) + if err != nil { + return contracts.WorkflowRegistryOpDeps{}, fmt.Errorf("failed to get MCMS contracts: %w", err) + } + mcmsContracts = loaded + } + + chain, ok := e.BlockChains.EVMChains()[chainSelector] + if !ok { + return contracts.WorkflowRegistryOpDeps{}, fmt.Errorf("chain with selector %d not found", chainSelector) + } + + registry, err := contracts.GetWorkflowRegistryV2FromDatastore(&e, chainSelector, qualifier) + if err != nil { + return contracts.WorkflowRegistryOpDeps{}, fmt.Errorf("failed to get workflow registry address from datastore: %w", err) + } + + strategy, err := strategies.CreateStrategy(chain, e, mcmsConfig, mcmsContracts, registry.Address(), description) + if err != nil { + return contracts.WorkflowRegistryOpDeps{}, fmt.Errorf("failed to create strategy: %w", err) + } + + return contracts.WorkflowRegistryOpDeps{ + Env: &e, + Strategy: strategy, + Registry: registry, + Chain: &chain, + }, nil +} + +// finalizeWorkflowRegistryOutput wraps a completed operation report into a ChangesetOutput, +// building an MCMS timelock proposal from mcmsOp when non-nil. +func finalizeWorkflowRegistryOutput( + strategy strategies.TransactionStrategy, + mcmsOp *types.BatchOperation, + report operations.Report[any, any], +) (cldf.ChangesetOutput, error) { + if mcmsOp == nil { + return cldf.ChangesetOutput{ + Reports: []operations.Report[any, any]{report}, + }, nil + } + proposal, err := strategy.BuildProposal([]types.BatchOperation{*mcmsOp}) + if err != nil { + return cldf.ChangesetOutput{}, fmt.Errorf("failed to build MCMS proposal: %w", err) + } + return cldf.ChangesetOutput{ + MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, + Reports: []operations.Report[any, any]{report}, + }, nil +} + // SetConfigInput configures metadata validation settings for workflow registry v2 type SetConfigInput struct { ChainSelector uint64 `json:"chainSelector"` @@ -405,45 +475,11 @@ func (l SetUserDONOverride) VerifyPreconditions(e cldf.Environment, config SetUs } func (l SetUserDONOverride) Apply(e cldf.Environment, config SetUserDONOverrideInput) (cldf.ChangesetOutput, error) { - // Get MCMS contracts if needed - var mcmsContracts *evmstate.MCMSWithTimelockState - if config.MCMSConfig != nil { - var err error - mcmsContracts, err = strategies.GetMCMSContracts(e, config.ChainSelector, *config.MCMSConfig) - if err != nil { - return cldf.ChangesetOutput{}, fmt.Errorf("failed to get MCMS contracts: %w", err) - } - } - - chain, ok := e.BlockChains.EVMChains()[config.ChainSelector] - if !ok { - return cldf.ChangesetOutput{}, fmt.Errorf("chain with selector %d not found", config.ChainSelector) - } - - registry, err := contracts.GetWorkflowRegistryV2FromDatastore(&e, config.ChainSelector, config.WorkflowRegistryQualifier) - if err != nil { - return cldf.ChangesetOutput{}, fmt.Errorf("failed to get workflow registry address from datastore: %w", err) - } - - // Create the appropriate strategy - strategy, err := strategies.CreateStrategy( - chain, - e, - config.MCMSConfig, - mcmsContracts, - registry.Address(), - contracts.SetUserDONOverrideDescription, - ) + deps, err := prepareWorkflowRegistryDeps(e, config.ChainSelector, config.WorkflowRegistryQualifier, config.MCMSConfig, contracts.SetUserDONOverrideDescription) if err != nil { - return cldf.ChangesetOutput{}, fmt.Errorf("failed to create strategy: %w", err) + return cldf.ChangesetOutput{}, err } - // Execute operation - deps := contracts.WorkflowRegistryOpDeps{ - Env: &e, - Strategy: strategy, - Registry: registry, - } report, err := operations.ExecuteOperation( e.OperationsBundle, contracts.SetUserDONOverrideOp, deps, contracts.SetUserDONOverrideOpInput{ @@ -460,21 +496,72 @@ func (l SetUserDONOverride) Apply(e cldf.Environment, config SetUserDONOverrideI return cldf.ChangesetOutput{}, err } - if report.Output.MCMSOperation != nil { - proposal, mcmsErr := strategy.BuildProposal([]types.BatchOperation{*report.Output.MCMSOperation}) - if mcmsErr != nil { - return cldf.ChangesetOutput{}, fmt.Errorf("failed to build MCMS proposal: %w", mcmsErr) + return finalizeWorkflowRegistryOutput(deps.Strategy, report.Output.MCMSOperation, report.ToGenericReport()) +} + +// BatchSetUserDONOverrideInput configures multiple user DON overrides in a single MCMS BatchOperation. +// +// This is functionally equivalent to applying SetUserDONOverride once per override entry, but produces +// a single MCMS BatchOperation containing N transactions instead of N separate batch operations. +// Use this when you have many overrides to apply to the same WorkflowRegistry to keep proposals compact +// and avoid blocking other queued proposals. +type BatchSetUserDONOverrideInput struct { + ChainSelector uint64 `json:"chainSelector"` + WorkflowRegistryQualifier string `json:"workflowRegistryQualifier"` // Qualifier to identify the specific workflow registry + Overrides []contracts.SetUserDONOverrideEntry `json:"overrides"` // Per-user overrides to apply + MCMSConfig *crecontracts.MCMSConfig `json:"mcmsConfig,omitempty"` // MCMS configuration +} + +type BatchSetUserDONOverride struct{} + +func (l BatchSetUserDONOverride) VerifyPreconditions(e cldf.Environment, config BatchSetUserDONOverrideInput) error { + if len(config.Overrides) == 0 { + return errors.New("must provide at least one override") + } + seen := make(map[string]struct{}, len(config.Overrides)) + for i, entry := range config.Overrides { + if entry.User == (common.Address{}) { + return fmt.Errorf("override[%d]: user address must not be zero", i) } + if entry.DONFamily == "" { + return fmt.Errorf("override[%d] (user %s): donFamily must not be empty", i, entry.User.Hex()) + } + key := entry.User.Hex() + "|" + entry.DONFamily + if _, dup := seen[key]; dup { + return fmt.Errorf("override[%d]: duplicate (user %s, donFamily %s)", i, entry.User.Hex(), entry.DONFamily) + } + seen[key] = struct{}{} + } + return nil +} - return cldf.ChangesetOutput{ - MCMSTimelockProposals: []mcms.TimelockProposal{*proposal}, - Reports: []operations.Report[any, any]{report.ToGenericReport()}, - }, nil +func (l BatchSetUserDONOverride) Apply(e cldf.Environment, config BatchSetUserDONOverrideInput) (cldf.ChangesetOutput, error) { + if err := l.VerifyPreconditions(e, config); err != nil { + return cldf.ChangesetOutput{}, err } - return cldf.ChangesetOutput{ - Reports: []operations.Report[any, any]{report.ToGenericReport()}, - }, nil + // The strategy is only used at the BuildProposal step inside finalizeWorkflowRegistryOutput; + // BatchSetUserDONOverrideOp itself builds calldata directly via deps.Chain + deps.Registry so + // all overrides land in a single MCMS BatchOperation. + deps, err := prepareWorkflowRegistryDeps(e, config.ChainSelector, config.WorkflowRegistryQualifier, config.MCMSConfig, contracts.BatchSetUserDONOverrideDescription) + if err != nil { + return cldf.ChangesetOutput{}, err + } + + report, err := operations.ExecuteOperation( + e.OperationsBundle, + contracts.BatchSetUserDONOverrideOp, deps, contracts.BatchSetUserDONOverrideOpInput{ + ChainSelector: config.ChainSelector, + Qualifier: config.WorkflowRegistryQualifier, + Overrides: config.Overrides, + MCMSConfig: config.MCMSConfig, + }, + ) + if err != nil { + return cldf.ChangesetOutput{}, err + } + + return finalizeWorkflowRegistryOutput(deps.Strategy, report.Output.MCMSOperation, report.ToGenericReport()) } // SetCapabilitiesRegistryInput configures the Capabilities registry address diff --git a/deployment/cre/workflow_registry/v2/changeset/configure_workflow_registry_test.go b/deployment/cre/workflow_registry/v2/changeset/configure_workflow_registry_test.go index 48a086f2e06..10261d3b247 100644 --- a/deployment/cre/workflow_registry/v2/changeset/configure_workflow_registry_test.go +++ b/deployment/cre/workflow_registry/v2/changeset/configure_workflow_registry_test.go @@ -7,7 +7,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" + mcmstypes "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + crecontracts "github.com/smartcontractkit/chainlink/deployment/cre/contracts" + "github.com/smartcontractkit/chainlink/deployment/cre/workflow_registry/v2/changeset/operations/contracts" ) func TestSetConfig(t *testing.T) { @@ -268,6 +273,203 @@ func TestSetUserDONOverride(t *testing.T) { }) } +func TestBatchSetUserDONOverride(t *testing.T) { + t.Parallel() + + // A small but non-trivial set of overrides reused across subtests. + makeOverrides := func() []contracts.SetUserDONOverrideEntry { + return []contracts.SetUserDONOverrideEntry{ + {User: common.HexToAddress("0x1111111111111111111111111111111111111111"), DONFamily: "test-don-family", Limit: 5, Enabled: true}, + {User: common.HexToAddress("0x2222222222222222222222222222222222222222"), DONFamily: "test-don-family", Limit: 7, Enabled: true}, + {User: common.HexToAddress("0x3333333333333333333333333333333333333333"), DONFamily: "test-don-family", Limit: 9, Enabled: false}, + } + } + + t.Run("verify preconditions: empty overrides rejected", func(t *testing.T) { + fixture := setupTest(t) + err := BatchSetUserDONOverride{}.VerifyPreconditions(fixture.rt.Environment(), BatchSetUserDONOverrideInput{ + ChainSelector: fixture.selector, + WorkflowRegistryQualifier: fixture.workflowRegistryQualifier, + Overrides: nil, + }) + require.Error(t, err) + }) + + t.Run("verify preconditions: zero user address rejected", func(t *testing.T) { + fixture := setupTest(t) + err := BatchSetUserDONOverride{}.VerifyPreconditions(fixture.rt.Environment(), BatchSetUserDONOverrideInput{ + ChainSelector: fixture.selector, + WorkflowRegistryQualifier: fixture.workflowRegistryQualifier, + Overrides: []contracts.SetUserDONOverrideEntry{ + {User: common.Address{}, DONFamily: "test-don-family", Limit: 1, Enabled: true}, + }, + }) + require.Error(t, err) + }) + + t.Run("verify preconditions: empty donFamily rejected", func(t *testing.T) { + fixture := setupTest(t) + err := BatchSetUserDONOverride{}.VerifyPreconditions(fixture.rt.Environment(), BatchSetUserDONOverrideInput{ + ChainSelector: fixture.selector, + WorkflowRegistryQualifier: fixture.workflowRegistryQualifier, + Overrides: []contracts.SetUserDONOverrideEntry{ + {User: common.HexToAddress("0x1111111111111111111111111111111111111111"), DONFamily: "", Limit: 1, Enabled: true}, + }, + }) + require.Error(t, err) + }) + + t.Run("verify preconditions: duplicate (user, donFamily) rejected", func(t *testing.T) { + fixture := setupTest(t) + dup := common.HexToAddress("0x1111111111111111111111111111111111111111") + err := BatchSetUserDONOverride{}.VerifyPreconditions(fixture.rt.Environment(), BatchSetUserDONOverrideInput{ + ChainSelector: fixture.selector, + WorkflowRegistryQualifier: fixture.workflowRegistryQualifier, + Overrides: []contracts.SetUserDONOverrideEntry{ + {User: dup, DONFamily: "test-don-family", Limit: 1, Enabled: true}, + {User: dup, DONFamily: "test-don-family", Limit: 2, Enabled: false}, + }, + }) + require.Error(t, err) + }) + + t.Run("batch set user DON override (no MCMS)", func(t *testing.T) { + fixture := setupTest(t) + + // set DON limit first + _, err := SetDONLimit{}.Apply(fixture.rt.Environment(), SetDONLimitInput{ + ChainSelector: fixture.selector, + WorkflowRegistryQualifier: fixture.workflowRegistryQualifier, + DONFamily: "test-don-family", + DONLimit: 100, + UserDefaultLimit: 5, + MCMSConfig: nil, + }) + require.NoError(t, err, "set DON limit should succeed") + + overrides := makeOverrides() + t.Log("Starting batch set user DON override...") + output, err := BatchSetUserDONOverride{}.Apply(fixture.rt.Environment(), BatchSetUserDONOverrideInput{ + ChainSelector: fixture.selector, + WorkflowRegistryQualifier: fixture.workflowRegistryQualifier, + Overrides: overrides, + MCMSConfig: nil, + }) + t.Logf("Batch set user DON override result: err=%v, output=%v", err, output) + require.NoError(t, err, "batch set user DON override should succeed") + require.NotNil(t, output, "output should not be nil") + // Non-MCMS path: each override was confirmed on-chain individually; no proposals expected. + require.Empty(t, output.MCMSTimelockProposals, "no MCMS proposals expected without MCMSConfig") + t.Log("Batch set user DON override completed successfully") + }) + + t.Run("batch set user DON override with MCMS produces single BatchOperation", func(t *testing.T) { + fixture := setupTestWithMCMS(t) + + // set DON limit first + _, err := SetDONLimit{}.Apply(fixture.rt.Environment(), SetDONLimitInput{ + ChainSelector: fixture.selector, + WorkflowRegistryQualifier: fixture.workflowRegistryQualifier, + DONFamily: "test-don-family", + DONLimit: 100, + UserDefaultLimit: 5, + MCMSConfig: nil, + }) + require.NoError(t, err, "set DON limit should succeed") + + overrides := makeOverrides() + t.Log("Starting batch set user DON override with MCMS...") + output, err := BatchSetUserDONOverride{}.Apply(fixture.rt.Environment(), BatchSetUserDONOverrideInput{ + ChainSelector: fixture.selector, + WorkflowRegistryQualifier: fixture.workflowRegistryQualifier, + Overrides: overrides, + MCMSConfig: &crecontracts.MCMSConfig{ + MinDelay: 30 * time.Second, + TimelockQualifierPerChain: map[uint64]string{ + fixture.selector: "", + }, + }, + }) + t.Logf("MCMS batch set user DON override result: err=%v, output=%v", err, output) + require.NoError(t, err, "MCMS batch set user DON override should succeed") + require.NotNil(t, output, "output should not be nil") + + // Core invariant: 1 proposal, 1 batch operation, N transactions inside. + require.Len(t, output.MCMSTimelockProposals, 1, "expected exactly one MCMS proposal") + require.Len(t, output.MCMSTimelockProposals[0].Operations, 1, "expected exactly one BatchOperation") + require.Len(t, output.MCMSTimelockProposals[0].Operations[0].Transactions, len(overrides), "BatchOperation should contain one Transaction per override") + t.Log("MCMS batch set user DON override completed successfully") + }) + + t.Run("op rejects input.ChainSelector that disagrees with deps.Chain.Selector", func(t *testing.T) { + // The defensive guard inside BatchSetUserDONOverrideOp prevents a programmatic caller + // (sequence, other changeset, future refactor) from generating a proposal whose target + // chain selector disagrees with the chain used to build the calldata. The public + // Apply() path can't trigger this — both values are derived from the same source — so + // we exercise the Op directly. + fixture := setupTest(t) + env := fixture.rt.Environment() + + deps, err := prepareWorkflowRegistryDeps(env, fixture.selector, fixture.workflowRegistryQualifier, nil, contracts.BatchSetUserDONOverrideDescription) + require.NoError(t, err, "prepareWorkflowRegistryDeps should succeed for the test fixture chain") + + _, err = operations.ExecuteOperation(env.OperationsBundle, contracts.BatchSetUserDONOverrideOp, deps, contracts.BatchSetUserDONOverrideOpInput{ + ChainSelector: fixture.selector + 1, // deliberately wrong; must not match deps.Chain.Selector + Qualifier: fixture.workflowRegistryQualifier, + Overrides: makeOverrides(), + }) + require.Error(t, err, "op must reject mismatched input.ChainSelector vs deps.Chain.Selector") + require.ErrorContains(t, err, "does not match deps.Chain.Selector") + }) + + t.Run("op works in MCMS path with nil deps.Chain", func(t *testing.T) { + // MCMS-only callers (e.g. a sequence assembling proposals) may legitimately have + // deps.Registry + strategy but no chain pointer. In that case the op should still + // build calldata via SimTransactOpts and produce a BatchOperation; input.ChainSelector + // is the authoritative source. + fixture := setupTestWithMCMS(t) + env := fixture.rt.Environment() + + mcmsConfig := &crecontracts.MCMSConfig{ + MinDelay: 30 * time.Second, + TimelockQualifierPerChain: map[uint64]string{ + fixture.selector: "", + }, + } + deps, err := prepareWorkflowRegistryDeps(env, fixture.selector, fixture.workflowRegistryQualifier, mcmsConfig, contracts.BatchSetUserDONOverrideDescription) + require.NoError(t, err, "prepareWorkflowRegistryDeps should succeed") + deps.Chain = nil // simulate an MCMS-only caller that didn't pass a chain pointer + + report, err := operations.ExecuteOperation(env.OperationsBundle, contracts.BatchSetUserDONOverrideOp, deps, contracts.BatchSetUserDONOverrideOpInput{ + ChainSelector: fixture.selector, + Qualifier: fixture.workflowRegistryQualifier, + Overrides: makeOverrides(), + MCMSConfig: mcmsConfig, + }) + require.NoError(t, err, "op should run in MCMS path without deps.Chain") + require.NotNil(t, report.Output.MCMSOperation, "MCMS BatchOperation should be produced") + require.Len(t, report.Output.MCMSOperation.Transactions, len(makeOverrides()), "BatchOperation should contain one Transaction per override") + require.Equal(t, mcmstypes.ChainSelector(fixture.selector), report.Output.MCMSOperation.ChainSelector, "BatchOperation chain selector must equal input.ChainSelector when deps.Chain is nil") + }) + + t.Run("op requires deps.Chain in non-MCMS path", func(t *testing.T) { + fixture := setupTest(t) + env := fixture.rt.Environment() + + deps, err := prepareWorkflowRegistryDeps(env, fixture.selector, fixture.workflowRegistryQualifier, nil, contracts.BatchSetUserDONOverrideDescription) + require.NoError(t, err, "prepareWorkflowRegistryDeps should succeed") + deps.Chain = nil // non-MCMS path needs DeployerKey + Confirm, so this must fail loudly + + _, err = operations.ExecuteOperation(env.OperationsBundle, contracts.BatchSetUserDONOverrideOp, deps, contracts.BatchSetUserDONOverrideOpInput{ + ChainSelector: fixture.selector, + Qualifier: fixture.workflowRegistryQualifier, + Overrides: makeOverrides(), + }) + require.Error(t, err, "op must reject nil deps.Chain in non-MCMS path") + require.ErrorContains(t, err, "deps.Chain is required when MCMSConfig is nil") + }) +} + func TestSetCapabilitiesRegistry(t *testing.T) { t.Parallel() diff --git a/deployment/cre/workflow_registry/v2/changeset/operations/contracts/configure_workflow_registry_ops.go b/deployment/cre/workflow_registry/v2/changeset/operations/contracts/configure_workflow_registry_ops.go index 8d352af5152..c277a8da529 100644 --- a/deployment/cre/workflow_registry/v2/changeset/operations/contracts/configure_workflow_registry_ops.go +++ b/deployment/cre/workflow_registry/v2/changeset/operations/contracts/configure_workflow_registry_ops.go @@ -3,6 +3,7 @@ package contracts import ( "errors" "fmt" + "math/big" "github.com/Masterminds/semver/v3" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -10,8 +11,10 @@ import ( "github.com/ethereum/go-ethereum/core/types" mcmstypes "github.com/smartcontractkit/mcms/types" + cldf_evm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" "github.com/smartcontractkit/chainlink-deployments-framework/datastore" cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + cldfproposalutils "github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/mcms/proposalutils" "github.com/smartcontractkit/chainlink-deployments-framework/operations" workflow_registry_v2 "github.com/smartcontractkit/chainlink-evm/gethwrappers/workflow/generated/workflow_registry_wrapper_v2" @@ -25,6 +28,7 @@ const ( SetWorkflowOwnerConfigDescription = "setWorkflowOwner config on workflow registry v2" SetDONLimitDescription = "setDonLimit on workflow registry v2" SetUserDONOverrideDescription = "setUserDonOverride on workflow registry v2" + BatchSetUserDONOverrideDescription = "batchSetUserDonOverride on workflow registry v2" SetCapabilitiesRegistryDescription = "setCapabilitiesRegistry on workflow registry v2" ) @@ -33,6 +37,14 @@ type WorkflowRegistryOpDeps struct { Env *cldf.Environment Strategy strategies.TransactionStrategy Registry *workflow_registry_v2.WorkflowRegistry + // Chain is the EVM chain whose datastore produced deps.Registry. Operations that bypass the + // strategy abstraction (e.g. BatchSetUserDONOverrideOp) require it when MCMSConfig is nil, + // to sign and confirm transactions on-chain via DeployerKey + Confirm. MCMS-only invocations + // of those operations may leave it nil; the chain selector then comes from the input. + // When provided, it must agree with the input's chain selector (the op validates this to + // avoid silently emitting a proposal targeting the wrong chain). Other operations that + // route everything through the strategy may leave it nil. + Chain *cldf_evm.Chain } // SetConfig Operation @@ -275,6 +287,116 @@ var SetUserDONOverrideOp = operations.NewOperation( }, ) +// BatchSetUserDONOverride Operation +// +// Iterates over Overrides and either confirms each setUserDONOverride transaction directly (no MCMS) +// or assembles a single MCMS BatchOperation containing one transaction per override. Using a single +// BatchOperation keeps the resulting proposal compact (1 batch, N transactions) instead of producing +// N separate batch operations as repeated SetUserDONOverride calls would. +type SetUserDONOverrideEntry struct { + User common.Address `json:"user"` + DONFamily string `json:"donFamily"` + Limit uint32 `json:"limit"` + Enabled bool `json:"enabled"` +} + +type BatchSetUserDONOverrideOpInput struct { + // ChainSelector and Qualifier are kept on the input to make the operation invocation uniquely + // identifiable (the registry itself is passed via deps). + ChainSelector uint64 `json:"chainSelector"` + Qualifier string `json:"qualifier"` + + Overrides []SetUserDONOverrideEntry `json:"overrides"` + MCMSConfig *contracts.MCMSConfig `json:"mcmsConfig,omitempty"` +} + +type BatchSetUserDONOverrideOpOutput struct { + Success bool `json:"success"` + RegistryAddress common.Address `json:"registryAddress"` + MCMSOperation *mcmstypes.BatchOperation `json:"mcmsOperation"` +} + +var BatchSetUserDONOverrideOp = operations.NewOperation( + "batch-set-user-don-override-op", + semver.MustParse("1.0.0"), + "Batch Set User DON Override in WorkflowRegistry V2", + func(b operations.Bundle, deps WorkflowRegistryOpDeps, input BatchSetUserDONOverrideOpInput) (BatchSetUserDONOverrideOpOutput, error) { + if len(input.Overrides) == 0 { + return BatchSetUserDONOverrideOpOutput{}, errors.New("must provide at least one override") + } + // deps.Chain is only strictly required for the non-MCMS path, where we need the deployer + // key to sign transactions and Confirm() to wait for them on-chain. In the MCMS path the + // op only builds calldata (via SimTransactOpts) and assembles an MCMS BatchOperation, so + // an MCMS-only caller can supply deps.Registry + strategy without a chain pointer. + if input.MCMSConfig == nil && deps.Chain == nil { + return BatchSetUserDONOverrideOpOutput{}, errors.New("deps.Chain is required when MCMSConfig is nil (needed to sign and confirm transactions on-chain)") + } + + // Resolve the authoritative chain selector. Prefer deps.Chain (the chain whose lookup + // produced deps.Registry) when present; that keeps the on-chain lookup and the proposal + // target trivially in sync. Fall back to input.ChainSelector for MCMS-only callers that + // deliberately don't pass a chain pointer. When both are present they must agree, else + // we'd silently emit a proposal targeting the wrong chain. + chainSelector := input.ChainSelector + if deps.Chain != nil { + chainSelector = deps.Chain.ChainSelector() + if input.ChainSelector != chainSelector { + return BatchSetUserDONOverrideOpOutput{}, fmt.Errorf( + "input.ChainSelector (%d) does not match deps.Chain.Selector (%d); refusing to build proposal with ambiguous target chain", + input.ChainSelector, chainSelector, + ) + } + } + + // MCMS path uses simulated tx opts to produce calldata without sending; non-MCMS uses the deployer key. + var txOpts *bind.TransactOpts + if input.MCMSConfig != nil { + txOpts = cldf.SimTransactOpts() + } else { + txOpts = deps.Chain.DeployerKey + } + + var mcmsTxs []mcmstypes.Transaction + for _, entry := range input.Overrides { + tx, err := deps.Registry.SetUserDONOverride(txOpts, entry.User, entry.DONFamily, entry.Limit, entry.Enabled) + if err != nil { + err = cldf.DecodeErr(workflow_registry_v2.WorkflowRegistryABI, err) + return BatchSetUserDONOverrideOpOutput{}, fmt.Errorf("failed to build SetUserDONOverride for %s: %w", entry.User.Hex(), err) + } + + if input.MCMSConfig == nil { + if _, cErr := deps.Chain.Confirm(tx); cErr != nil { + return BatchSetUserDONOverrideOpOutput{}, fmt.Errorf("failed to confirm SetUserDONOverride for %s (tx %s): %w", entry.User.Hex(), tx.Hash().String(), cErr) + } + continue + } + + mtx, err := cldfproposalutils.TransactionForChain(chainSelector, deps.Registry.Address().Hex(), tx.Data(), big.NewInt(0), "", nil) + if err != nil { + return BatchSetUserDONOverrideOpOutput{}, fmt.Errorf("failed to build MCMS transaction for %s: %w", entry.User.Hex(), err) + } + mcmsTxs = append(mcmsTxs, mtx) + } + + var mergedOp *mcmstypes.BatchOperation + if input.MCMSConfig != nil { + mergedOp = &mcmstypes.BatchOperation{ + ChainSelector: mcmstypes.ChainSelector(chainSelector), + Transactions: mcmsTxs, + } + deps.Env.Logger.Infof("Created MCMS batch with %d SetUserDONOverride transactions on chain %d", len(mcmsTxs), chainSelector) + } else { + deps.Env.Logger.Infof("Successfully applied %d SetUserDONOverride transactions on chain %d", len(input.Overrides), chainSelector) + } + + return BatchSetUserDONOverrideOpOutput{ + Success: true, + MCMSOperation: mergedOp, + RegistryAddress: deps.Registry.Address(), + }, nil + }, +) + // SetCapabilitiesRegistry MCMSOperation type SetCapabilitiesRegistryOpInput struct { // We are passing the registry via the deps, but we keep chainSelector and qualifier to allow the operation to be