diff --git a/.changeset/tall-wolves-open.md b/.changeset/tall-wolves-open.md new file mode 100644 index 00000000..7d52a238 --- /dev/null +++ b/.changeset/tall-wolves-open.md @@ -0,0 +1,5 @@ +--- +"chainlink-deployments-framework": minor +--- + +Add inspectors and converters helpers for mcms chain client loaders diff --git a/.mockery.yml b/.mockery.yml index 746694b8..fb36ab6c 100644 --- a/.mockery.yml +++ b/.mockery.yml @@ -107,6 +107,14 @@ packages: structname: "{{.Mock}}{{.InterfaceName}}" interfaces: ProposalContext: + github.com/smartcontractkit/chainlink-deployments-framework/experimental/proposalutils: + config: + all: false + dir: "{{.InterfaceDir}}/mocks" + filename: "mock_{{.InterfaceName | snakecase}}.go" + structname: "{{.Mock}}{{.InterfaceName}}" + interfaces: + InspectorFetcher: github.com/smartcontractkit/chainlink-deployments-framework/engine/cld/legacy/cli/mcmsv2: interfaces: commandRunnerI: diff --git a/experimental/proposalutils/chainsmetadata/aptos_helpers.go b/experimental/proposalutils/chainsmetadata/aptos_helpers.go new file mode 100644 index 00000000..b41517af --- /dev/null +++ b/experimental/proposalutils/chainsmetadata/aptos_helpers.go @@ -0,0 +1,35 @@ +package chainsmetadata + +import ( + "errors" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk/aptos" + "github.com/smartcontractkit/mcms/types" +) + +func AptosRoleFromAction(action types.TimelockAction) (aptos.TimelockRole, error) { + switch action { + case types.TimelockActionBypass: + return aptos.TimelockRoleBypasser, nil + case types.TimelockActionSchedule: + return aptos.TimelockRoleProposer, nil + case types.TimelockActionCancel: + return aptos.TimelockRoleCanceller, nil + default: + return 0, errors.New("unknown timelock action") + } +} + +func AptosRoleFromProposal(proposal *mcms.TimelockProposal) (aptos.TimelockRole, error) { + if proposal == nil { + return 0, errors.New("aptos timelock proposal is needed") + } + + role, err := AptosRoleFromAction(proposal.Action) + if err != nil { + return 0, err + } + + return role, nil +} diff --git a/experimental/proposalutils/chainsmetadata/aptos_helpers_test.go b/experimental/proposalutils/chainsmetadata/aptos_helpers_test.go new file mode 100644 index 00000000..0ea732cf --- /dev/null +++ b/experimental/proposalutils/chainsmetadata/aptos_helpers_test.go @@ -0,0 +1,135 @@ +package chainsmetadata + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk/aptos" + mcmsTypes "github.com/smartcontractkit/mcms/types" +) + +func TestAptosRoleFromAction(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + action mcmsTypes.TimelockAction + expectedRole aptos.TimelockRole + expectError bool + }{ + { + name: "bypass action returns bypasser role", + action: mcmsTypes.TimelockActionBypass, + expectedRole: aptos.TimelockRoleBypasser, + expectError: false, + }, + { + name: "schedule action returns proposer role", + action: mcmsTypes.TimelockActionSchedule, + expectedRole: aptos.TimelockRoleProposer, + expectError: false, + }, + { + name: "cancel action returns canceller role", + action: mcmsTypes.TimelockActionCancel, + expectedRole: aptos.TimelockRoleCanceller, + expectError: false, + }, + { + name: "unknown action returns error", + action: mcmsTypes.TimelockAction("unknown"), + expectedRole: 0, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + role, err := AptosRoleFromAction(tt.action) + + if tt.expectError { + require.Error(t, err) + assert.Equal(t, "unknown timelock action", err.Error()) + assert.Equal(t, tt.expectedRole, role) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedRole, role) + } + }) + } +} + +func TestAptosRoleFromProposal(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + proposal *mcms.TimelockProposal + expectedRole aptos.TimelockRole + expectError bool + errorMsg string + }{ + { + name: "nil proposal returns error", + proposal: nil, + expectedRole: 0, + expectError: true, + errorMsg: "aptos timelock proposal is needed", + }, + { + name: "proposal with bypass action returns bypasser role", + proposal: &mcms.TimelockProposal{ + Action: mcmsTypes.TimelockActionBypass, + }, + expectedRole: aptos.TimelockRoleBypasser, + expectError: false, + }, + { + name: "proposal with schedule action returns proposer role", + proposal: &mcms.TimelockProposal{ + Action: mcmsTypes.TimelockActionSchedule, + }, + expectedRole: aptos.TimelockRoleProposer, + expectError: false, + }, + { + name: "proposal with cancel action returns canceller role", + proposal: &mcms.TimelockProposal{ + Action: mcmsTypes.TimelockActionCancel, + }, + expectedRole: aptos.TimelockRoleCanceller, + expectError: false, + }, + { + name: "proposal with unknown action returns error", + proposal: &mcms.TimelockProposal{ + Action: mcmsTypes.TimelockAction("unknown"), + }, + expectedRole: 0, + expectError: true, + errorMsg: "unknown timelock action", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + role, err := AptosRoleFromProposal(tt.proposal) + + if tt.expectError { + require.Error(t, err) + assert.Equal(t, tt.errorMsg, err.Error()) + assert.Equal(t, aptos.TimelockRole(0), role) + } else { + require.NoError(t, err) + assert.Equal(t, tt.expectedRole, role) + } + }) + } +} diff --git a/experimental/proposalutils/chainsmetadata/sui_helpers.go b/experimental/proposalutils/chainsmetadata/sui_helpers.go new file mode 100644 index 00000000..77f4dca9 --- /dev/null +++ b/experimental/proposalutils/chainsmetadata/sui_helpers.go @@ -0,0 +1,30 @@ +package chainsmetadata + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk/sui" + "github.com/smartcontractkit/mcms/types" +) + +func SuiMetadataFromProposal(selector types.ChainSelector, proposal *mcms.TimelockProposal) (sui.AdditionalFieldsMetadata, error) { + if proposal == nil { + return sui.AdditionalFieldsMetadata{}, errors.New("sui timelock proposal is needed") + } + + var metadata sui.AdditionalFieldsMetadata + err := json.Unmarshal([]byte(proposal.ChainMetadata[selector].AdditionalFields), &metadata) + if err != nil { + return sui.AdditionalFieldsMetadata{}, fmt.Errorf("error unmarshaling sui chain metadata: %w", err) + } + + err = metadata.Validate() + if err != nil { + return sui.AdditionalFieldsMetadata{}, fmt.Errorf("error validating sui chain metadata: %w", err) + } + + return metadata, nil +} diff --git a/experimental/proposalutils/chainsmetadata/sui_helpers_test.go b/experimental/proposalutils/chainsmetadata/sui_helpers_test.go new file mode 100644 index 00000000..30838114 --- /dev/null +++ b/experimental/proposalutils/chainsmetadata/sui_helpers_test.go @@ -0,0 +1,121 @@ +package chainsmetadata + +import ( + "encoding/json" + "testing" + + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk/sui" + "github.com/smartcontractkit/mcms/types" +) + +func buildTestProposalWithMetadata(chainSelector types.ChainSelector, additionalFields string) *mcms.TimelockProposal { + builder := mcms.NewTimelockProposalBuilder() + builder.SetVersion("v1"). + SetAction(types.TimelockActionSchedule). + SetValidUntil(4000000000). + AddTimelockAddress(chainSelector, "0x1234567890123456789012345678901234567890"). + AddChainMetadata(chainSelector, types.ChainMetadata{ + AdditionalFields: []byte(additionalFields), + }). + AddOperation(types.BatchOperation{ + ChainSelector: chainSelector, + Transactions: []types.Transaction{ + { + To: "0x1234567890123456789012345678901234567890", + Data: []byte{0x01}, + AdditionalFields: json.RawMessage(`{}`), + }, + }, + }) + proposal, err := builder.Build() + if err != nil { + panic(err) + } + + return proposal +} + +func TestSuiMetadataFromProposal(t *testing.T) { + t.Parallel() + + chainSelector := types.ChainSelector(chainsel.GETH_TESTNET.Selector) + validMetadataJSON := `{"mcms_package_id":"0x1","role":1,"account_obj":"0x2","registry_obj":"0x3","timelock_obj":"0x4","deployer_state_obj":"0x5"}` + invalidJSON := `{"mcms_package_id":"0x1","role":1` + missingFieldsJSON := `{"role":1}` + + tests := []struct { + name string + selector types.ChainSelector + proposal *mcms.TimelockProposal + expectError bool + errorMsg string + }{ + { + name: "nil proposal returns error", + selector: chainSelector, + proposal: nil, + expectError: true, + errorMsg: "sui timelock proposal is needed", + }, + { + name: "valid metadata returns success", + selector: chainSelector, + proposal: buildTestProposalWithMetadata(chainSelector, validMetadataJSON), + expectError: false, + }, + { + name: "invalid JSON returns error", + selector: chainSelector, + proposal: buildTestProposalWithMetadata(chainSelector, invalidJSON), + expectError: true, + errorMsg: "error unmarshaling sui chain metadata", + }, + { + name: "missing required fields returns validation error", + selector: chainSelector, + proposal: buildTestProposalWithMetadata(chainSelector, missingFieldsJSON), + expectError: true, + errorMsg: "error validating sui chain metadata", + }, + { + name: "empty additional fields returns unmarshaling error", + selector: chainSelector, + proposal: buildTestProposalWithMetadata(chainSelector, ""), + expectError: true, + errorMsg: "error unmarshaling sui chain metadata", + }, + { + name: "missing chain selector in metadata", + selector: types.ChainSelector(999), + proposal: buildTestProposalWithMetadata(chainSelector, validMetadataJSON), + expectError: true, + errorMsg: "error unmarshaling sui chain metadata", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + metadata, err := SuiMetadataFromProposal(tt.selector, tt.proposal) + + if tt.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorMsg) + assert.Equal(t, sui.AdditionalFieldsMetadata{}, metadata) + } else { + require.NoError(t, err) + assert.Equal(t, "0x1", metadata.McmsPackageID) + assert.Equal(t, sui.TimelockRole(1), metadata.Role) + assert.Equal(t, "0x2", metadata.AccountObj) + assert.Equal(t, "0x3", metadata.RegistryObj) + assert.Equal(t, "0x4", metadata.TimelockObj) + } + }) + } +} diff --git a/experimental/proposalutils/converters.go b/experimental/proposalutils/converters.go new file mode 100644 index 00000000..e0e0562b --- /dev/null +++ b/experimental/proposalutils/converters.go @@ -0,0 +1,40 @@ +package proposalutils + +import ( + "fmt" + + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/smartcontractkit/mcms" + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/sdk/aptos" + "github.com/smartcontractkit/mcms/sdk/evm" + "github.com/smartcontractkit/mcms/sdk/solana" + "github.com/smartcontractkit/mcms/types" +) + +// BuildConvertersForTimelockProposal constructs a map of chain selectors to their respective timelock converters based on the provided timelock proposal. +func BuildConvertersForTimelockProposal(proposal mcms.TimelockProposal) (map[types.ChainSelector]sdk.TimelockConverter, error) { + converters := make(map[types.ChainSelector]sdk.TimelockConverter) + for chainMeta := range proposal.ChainMetadata { + fam, err := types.GetChainSelectorFamily(chainMeta) + if err != nil { + return nil, fmt.Errorf("error getting chain family: %w", err) + } + + var converter sdk.TimelockConverter + switch fam { + case chainsel.FamilyEVM: + converter = &evm.TimelockConverter{} + case chainsel.FamilySolana: + converter = solana.TimelockConverter{} + case chainsel.FamilyAptos: + converter = aptos.NewTimelockConverter() + default: + return nil, fmt.Errorf("unsupported chain family %s", fam) + } + + converters[chainMeta] = converter + } + + return converters, nil +} diff --git a/experimental/proposalutils/inspectors.go b/experimental/proposalutils/inspectors.go new file mode 100644 index 00000000..f56aaa2e --- /dev/null +++ b/experimental/proposalutils/inspectors.go @@ -0,0 +1,75 @@ +package proposalutils + +import ( + "fmt" + + chainsel "github.com/smartcontractkit/chain-selectors" + + cldfChain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + + "github.com/smartcontractkit/mcms/sdk/aptos" + "github.com/smartcontractkit/mcms/sdk/evm" + "github.com/smartcontractkit/mcms/sdk/solana" + + "github.com/smartcontractkit/mcms/sdk" + mcmsTypes "github.com/smartcontractkit/mcms/types" + + "github.com/smartcontractkit/chainlink-deployments-framework/experimental/proposalutils/chainsmetadata" +) + +type InspectorFetcher interface { + FetchInspectors(chainMetadata map[mcmsTypes.ChainSelector]mcmsTypes.ChainMetadata, action mcmsTypes.TimelockAction) (map[mcmsTypes.ChainSelector]sdk.Inspector, error) +} + +var _ InspectorFetcher = (*MCMInspectorFetcher)(nil) + +type MCMInspectorFetcher struct { + chains cldfChain.BlockChains +} + +func NewMCMInspectorFetcher(chains cldfChain.BlockChains) *MCMInspectorFetcher { + return &MCMInspectorFetcher{chains: chains} +} + +// FetchInspectors gets a map of inspectors for the given chain metadata and chain clients +func (b *MCMInspectorFetcher) FetchInspectors( + chainMetadata map[mcmsTypes.ChainSelector]mcmsTypes.ChainMetadata, + action mcmsTypes.TimelockAction) (map[mcmsTypes.ChainSelector]sdk.Inspector, error) { + inspectors := map[mcmsTypes.ChainSelector]sdk.Inspector{} + for chainSelector := range chainMetadata { + inspector, err := GetInspectorFromChainSelector(b.chains, uint64(chainSelector), action) + if err != nil { + return nil, fmt.Errorf("error getting inspector for chain selector %d: %w", chainSelector, err) + } + inspectors[chainSelector] = inspector + } + + return inspectors, nil +} + +// GetInspectorFromChainSelector returns an inspector for the given chain selector and chain clients +func GetInspectorFromChainSelector(chains cldfChain.BlockChains, selector uint64, action mcmsTypes.TimelockAction) (sdk.Inspector, error) { + fam, err := mcmsTypes.GetChainSelectorFamily(mcmsTypes.ChainSelector(selector)) + if err != nil { + return nil, fmt.Errorf("error getting chainClient family: %w", err) + } + + var inspector sdk.Inspector + switch fam { + case chainsel.FamilyEVM: + inspector = evm.NewInspector(chains.EVMChains()[selector].Client) + case chainsel.FamilySolana: + inspector = solana.NewInspector(chains.SolanaChains()[selector].Client) + case chainsel.FamilyAptos: + role, err := chainsmetadata.AptosRoleFromAction(action) + if err != nil { + return nil, fmt.Errorf("error getting aptos role from proposal: %w", err) + } + chainClient := chains.AptosChains()[selector] + inspector = aptos.NewInspector(chainClient.Client, role) + default: + return nil, fmt.Errorf("unsupported chain family %s", fam) + } + + return inspector, nil +} diff --git a/experimental/proposalutils/inspectors_test.go b/experimental/proposalutils/inspectors_test.go new file mode 100644 index 00000000..662b39d1 --- /dev/null +++ b/experimental/proposalutils/inspectors_test.go @@ -0,0 +1,85 @@ +package proposalutils + +import ( + "testing" + + chainsel "github.com/smartcontractkit/chain-selectors" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldfevm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldfsol "github.com/smartcontractkit/chainlink-deployments-framework/chain/solana" + + mcmsTypes "github.com/smartcontractkit/mcms/types" +) + +func TestMCMInspectorBuilder_BuildInspectors(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + chainMetadata map[mcmsTypes.ChainSelector]mcmsTypes.ChainMetadata + chainClientsEVM map[uint64]cldfevm.Chain + chainClientsSolana map[uint64]cldfsol.Chain + expectErr bool + errContains string + expectedInspectorsCount int + }{ + { + name: "empty input", + chainMetadata: map[mcmsTypes.ChainSelector]mcmsTypes.ChainMetadata{}, + chainClientsEVM: map[uint64]cldfevm.Chain{ + chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector: {Client: nil, Selector: chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector}, + }, + expectErr: false, + }, + { + name: "missing chain client", + chainMetadata: map[mcmsTypes.ChainSelector]mcmsTypes.ChainMetadata{ + 1: {MCMAddress: "0xabc", StartingOpCount: 0}, + }, + chainClientsEVM: map[uint64]cldfevm.Chain{}, + expectErr: true, + errContains: "error getting inspector for chain selector 1: error getting chainClient family: chain family not found for selector 1", + }, + { + name: "valid input", + chainMetadata: map[mcmsTypes.ChainSelector]mcmsTypes.ChainMetadata{ + mcmsTypes.ChainSelector(chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector): {MCMAddress: "0xabc", StartingOpCount: 0}, + mcmsTypes.ChainSelector(chainsel.SOLANA_DEVNET.Selector): {MCMAddress: "0xabc", StartingOpCount: 0}, + }, + chainClientsEVM: map[uint64]cldfevm.Chain{ + chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector: {Selector: chainsel.ETHEREUM_TESTNET_SEPOLIA.Selector}, + }, + chainClientsSolana: map[uint64]cldfsol.Chain{ + chainsel.SOLANA_DEVNET.Selector: {Selector: chainsel.SOLANA_DEVNET.Selector}, + }, + expectErr: false, + expectedInspectorsCount: 2, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + allChains := map[uint64]chain.BlockChain{} + // Populate EVM chains + for _, evmChain := range tc.chainClientsEVM { + allChains[evmChain.ChainSelector()] = evmChain + } + // Populate SOL chains + for _, solChain := range tc.chainClientsSolana { + allChains[solChain.ChainSelector()] = solChain + } + builder := NewMCMInspectorFetcher(chain.NewBlockChains(allChains)) + inspectors, err := builder.FetchInspectors(tc.chainMetadata, mcmsTypes.TimelockActionSchedule) + if tc.expectErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errContains) + } else { + require.NoError(t, err) + require.Len(t, inspectors, tc.expectedInspectorsCount) + } + }) + } +} diff --git a/experimental/proposalutils/mocks/mock_inspector_fetcher.go b/experimental/proposalutils/mocks/mock_inspector_fetcher.go new file mode 100644 index 00000000..327e6edd --- /dev/null +++ b/experimental/proposalutils/mocks/mock_inspector_fetcher.go @@ -0,0 +1,106 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package proposalutils + +import ( + "github.com/smartcontractkit/mcms/sdk" + "github.com/smartcontractkit/mcms/types" + mock "github.com/stretchr/testify/mock" +) + +// NewMockInspectorFetcher creates a new instance of MockInspectorFetcher. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockInspectorFetcher(t interface { + mock.TestingT + Cleanup(func()) +}) *MockInspectorFetcher { + mock := &MockInspectorFetcher{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// MockInspectorFetcher is an autogenerated mock type for the InspectorFetcher type +type MockInspectorFetcher struct { + mock.Mock +} + +type MockInspectorFetcher_Expecter struct { + mock *mock.Mock +} + +func (_m *MockInspectorFetcher) EXPECT() *MockInspectorFetcher_Expecter { + return &MockInspectorFetcher_Expecter{mock: &_m.Mock} +} + +// FetchInspectors provides a mock function for the type MockInspectorFetcher +func (_mock *MockInspectorFetcher) FetchInspectors(chainMetadata map[types.ChainSelector]types.ChainMetadata, action types.TimelockAction) (map[types.ChainSelector]sdk.Inspector, error) { + ret := _mock.Called(chainMetadata, action) + + if len(ret) == 0 { + panic("no return value specified for FetchInspectors") + } + + var r0 map[types.ChainSelector]sdk.Inspector + var r1 error + if returnFunc, ok := ret.Get(0).(func(map[types.ChainSelector]types.ChainMetadata, types.TimelockAction) (map[types.ChainSelector]sdk.Inspector, error)); ok { + return returnFunc(chainMetadata, action) + } + if returnFunc, ok := ret.Get(0).(func(map[types.ChainSelector]types.ChainMetadata, types.TimelockAction) map[types.ChainSelector]sdk.Inspector); ok { + r0 = returnFunc(chainMetadata, action) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[types.ChainSelector]sdk.Inspector) + } + } + if returnFunc, ok := ret.Get(1).(func(map[types.ChainSelector]types.ChainMetadata, types.TimelockAction) error); ok { + r1 = returnFunc(chainMetadata, action) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// MockInspectorFetcher_FetchInspectors_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FetchInspectors' +type MockInspectorFetcher_FetchInspectors_Call struct { + *mock.Call +} + +// FetchInspectors is a helper method to define mock.On call +// - chainMetadata map[types.ChainSelector]types.ChainMetadata +// - action types.TimelockAction +func (_e *MockInspectorFetcher_Expecter) FetchInspectors(chainMetadata interface{}, action interface{}) *MockInspectorFetcher_FetchInspectors_Call { + return &MockInspectorFetcher_FetchInspectors_Call{Call: _e.mock.On("FetchInspectors", chainMetadata, action)} +} + +func (_c *MockInspectorFetcher_FetchInspectors_Call) Run(run func(chainMetadata map[types.ChainSelector]types.ChainMetadata, action types.TimelockAction)) *MockInspectorFetcher_FetchInspectors_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 map[types.ChainSelector]types.ChainMetadata + if args[0] != nil { + arg0 = args[0].(map[types.ChainSelector]types.ChainMetadata) + } + var arg1 types.TimelockAction + if args[1] != nil { + arg1 = args[1].(types.TimelockAction) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *MockInspectorFetcher_FetchInspectors_Call) Return(chainSelectorToInspector map[types.ChainSelector]sdk.Inspector, err error) *MockInspectorFetcher_FetchInspectors_Call { + _c.Call.Return(chainSelectorToInspector, err) + return _c +} + +func (_c *MockInspectorFetcher_FetchInspectors_Call) RunAndReturn(run func(chainMetadata map[types.ChainSelector]types.ChainMetadata, action types.TimelockAction) (map[types.ChainSelector]sdk.Inspector, error)) *MockInspectorFetcher_FetchInspectors_Call { + _c.Call.Return(run) + return _c +} diff --git a/go.mod b/go.mod index 8b317099..4de0709c 100644 --- a/go.mod +++ b/go.mod @@ -241,7 +241,7 @@ require ( github.com/rs/zerolog v1.34.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/samber/lo v1.49.1 // indirect + github.com/samber/lo v1.49.1 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect github.com/scylladb/go-reflectx v1.0.1 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect