diff --git a/.changeset/giant-plums-dress.md b/.changeset/giant-plums-dress.md new file mode 100644 index 00000000..7a7e5524 --- /dev/null +++ b/.changeset/giant-plums-dress.md @@ -0,0 +1,5 @@ +--- +"@smartcontractkit/mcms": minor +--- + +Adds support for getMinDelay to timelock inspectors on all supported chain families diff --git a/e2e/tests/aptos/timelock_proposal.go b/e2e/tests/aptos/timelock_proposal.go index b85ae0b7..ce23f73f 100644 --- a/e2e/tests/aptos/timelock_proposal.go +++ b/e2e/tests/aptos/timelock_proposal.go @@ -9,6 +9,7 @@ import ( "slices" "time" + "github.com/aptos-labs/aptos-go-sdk/api" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/smartcontractkit/chainlink-aptos/bindings/bind" @@ -67,7 +68,11 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() { a.Require().NoError(err) a.Require().True(data.Success, data.VmStatus) } - + // Get Min Delay + timelockInspector := aptossdk.NewTimelockInspector(a.AptosRPCClient) + delay, err := timelockInspector.GetMinDelay(a.T().Context(), mcmsAddress.StringLong()) + a.Require().NoError(err) + a.Require().Equal(uint64(0), delay) // Configure Proposers proposers := [3]common.Address{} proposerKeys := [3]*ecdsa.PrivateKey{} @@ -84,18 +89,22 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() { Signers: proposers[:], } proposeConfigurer := aptossdk.NewConfigurer(a.AptosRPCClient, a.deployerAccount, aptossdk.TimelockRoleProposer) - result, err := proposeConfigurer.SetConfig(a.T().Context(), mcmsAddress.StringLong(), proposerConfig, false) + var result types.TransactionResult + result, err = proposeConfigurer.SetConfig(a.T().Context(), mcmsAddress.StringLong(), proposerConfig, false) a.Require().NoError(err) - data, err := a.AptosRPCClient.WaitForTransaction(result.Hash) + var data *api.UserTransaction + data, err = a.AptosRPCClient.WaitForTransaction(result.Hash) a.Require().NoError(err) a.Require().True(data.Success, data.VmStatus) } // Initiate ownership transfer { - tx, err := a.MCMSContract.MCMSAccount().TransferOwnershipToSelf(opts) + var tx *api.PendingTransaction + tx, err = a.MCMSContract.MCMSAccount().TransferOwnershipToSelf(opts) a.Require().NoError(err) - data, err := a.AptosRPCClient.WaitForTransaction(tx.Hash) + var data *api.UserTransaction + data, err = a.AptosRPCClient.WaitForTransaction(tx.Hash) a.Require().NoError(err) a.Require().True(data.Success, data.VmStatus) a.T().Logf("🚀 TransferOwnershipToSelf in tx: %s", tx.Hash) @@ -120,9 +129,9 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() { SetAction(types.TimelockActionSchedule). SetDelay(types.NewDuration(time.Second * 2)) - module, function, _, args, err := a.MCMSContract.MCMSAccount().Encoder().AcceptOwnership() - a.Require().NoError(err) - transaction, err := aptossdk.NewTransaction( + module, function, _, args, errAccept := a.MCMSContract.MCMSAccount().Encoder().AcceptOwnership() + a.Require().NoError(errAccept) + transaction, errTx := aptossdk.NewTransaction( module.PackageName, module.ModuleName, function, @@ -131,25 +140,28 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() { "MCMS", nil, ) - a.Require().NoError(err) + a.Require().NoError(errTx) acceptOwnershipProposalBuilder.AddOperation(types.BatchOperation{ ChainSelector: a.ChainSelector, Transactions: []types.Transaction{transaction}, }) - acceptOwnershipTimelockProposal, err := acceptOwnershipProposalBuilder.Build() + var acceptOwnershipTimelockProposal *mcms.TimelockProposal + acceptOwnershipTimelockProposal, err = acceptOwnershipProposalBuilder.Build() a.Require().NoError(err) convertersMap := map[types.ChainSelector]sdk.TimelockConverter{ a.ChainSelector: aptossdk.NewTimelockConverter(), } - acceptOwnershipProposal, _, err := acceptOwnershipTimelockProposal.Convert(a.T().Context(), convertersMap) + var acceptOwnershipProposal mcms.Proposal + acceptOwnershipProposal, _, err = acceptOwnershipTimelockProposal.Convert(a.T().Context(), convertersMap) a.Require().NoError(err) inspector := aptossdk.NewInspector(a.AptosRPCClient, aptossdk.TimelockRoleProposer) inspectorsMap := map[types.ChainSelector]sdk.Inspector{ a.ChainSelector: inspector, } - signable, err := mcms.NewSignable(&acceptOwnershipProposal, inspectorsMap) + var signable *mcms.Signable + signable, err = mcms.NewSignable(&acceptOwnershipProposal, inspectorsMap) a.Require().NoError(err) _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(proposerKeys[0])) @@ -159,32 +171,37 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() { _, err = signable.SignAndAppend(mcms.NewPrivateKeySigner(proposerKeys[2])) a.Require().NoError(err) - quorumMet, err := signable.ValidateSignatures(a.T().Context()) + var quorumMet bool + quorumMet, err = signable.ValidateSignatures(a.T().Context()) a.Require().NoError(err, "Error validating signatures") a.Require().True(quorumMet, "Quorum not met") // Set Root - encoders, err := acceptOwnershipProposal.GetEncoders() + var encoders map[types.ChainSelector]sdk.Encoder + encoders, err = acceptOwnershipProposal.GetEncoders() a.Require().NoError(err) aptosEncoder := encoders[a.ChainSelector].(*aptossdk.Encoder) executors := map[types.ChainSelector]sdk.Executor{ a.ChainSelector: aptossdk.NewExecutor(a.AptosRPCClient, a.deployerAccount, aptosEncoder, aptossdk.TimelockRoleProposer), } - executable, err := mcms.NewExecutable(&acceptOwnershipProposal, executors) + var executable *mcms.Executable + executable, err = mcms.NewExecutable(&acceptOwnershipProposal, executors) a.Require().NoError(err, "Error creating executable") - result, err := executable.SetRoot(a.T().Context(), a.ChainSelector) + var result types.TransactionResult + result, err = executable.SetRoot(a.T().Context(), a.ChainSelector) a.Require().NoError(err) - data, err := a.AptosRPCClient.WaitForTransaction(result.Hash) + var data *api.UserTransaction + data, err = a.AptosRPCClient.WaitForTransaction(result.Hash) a.Require().NoError(err) a.Require().True(data.Success, data.VmStatus) a.T().Logf("✅ SetRoot in tx: %s", result.Hash) // Assert tree, _ := acceptOwnershipProposal.MerkleTree() - gotHash, gotValidUntil, err := inspector.GetRoot(a.T().Context(), mcmsAddress.StringLong()) - a.Require().NoError(err) + gotHash, gotValidUntil, errSetRoot := inspector.GetRoot(a.T().Context(), mcmsAddress.StringLong()) + a.Require().NoError(errSetRoot) a.Require().Equal(validUntil, gotValidUntil) a.Require().Equal(tree.Root, gotHash) @@ -212,14 +229,17 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() { timelockExecutors := map[types.ChainSelector]sdk.TimelockExecutor{ a.ChainSelector: timelockExecutor, } - timelockExecutable, err := mcms.NewTimelockExecutable(a.T().Context(), acceptOwnershipTimelockProposal, timelockExecutors) + var timelockExecutable *mcms.TimelockExecutable + timelockExecutable, err = mcms.NewTimelockExecutable(a.T().Context(), acceptOwnershipTimelockProposal, timelockExecutors) a.Require().NoError(err) - operationID, err := timelockExecutable.GetOpID(a.T().Context(), 0, acceptOwnershipTimelockProposal.Operations[0], a.ChainSelector) + var operationID common.Hash + operationID, err = timelockExecutable.GetOpID(a.T().Context(), 0, acceptOwnershipTimelockProposal.Operations[0], a.ChainSelector) a.Require().NoError(err) timelockInspector := aptossdk.NewTimelockInspector(a.AptosRPCClient) - ok, err := timelockInspector.IsOperation(a.T().Context(), mcmsAddress.StringLong(), operationID) - a.Require().NoError(err) + + ok, errIsOp := timelockInspector.IsOperation(a.T().Context(), mcmsAddress.StringLong(), operationID) + a.Require().NoError(errIsOp) a.Require().True(ok, "Operation not found in timelock") a.Require().EventuallyWithT( @@ -234,8 +254,8 @@ func (a *AptosTestSuite) Test_Aptos_TimelockProposal() { a.T().Logf("🟢 Timelock operation ready, elapsed time: %v", elapsed.String()) for i := range acceptOwnershipTimelockProposal.Operations { - res, err := timelockExecutable.Execute(a.T().Context(), i) - a.Require().NoError(err) + res, errExec := timelockExecutable.Execute(a.T().Context(), i) + a.Require().NoError(errExec) data, err = a.AptosRPCClient.WaitForTransaction(res.Hash) a.Require().NoError(err) a.Require().True(data.Success, data.VmStatus) diff --git a/e2e/tests/evm/timelock_inspection.go b/e2e/tests/evm/timelock_inspection.go index d1f4395a..bd3f0676 100644 --- a/e2e/tests/evm/timelock_inspection.go +++ b/e2e/tests/evm/timelock_inspection.go @@ -316,3 +316,13 @@ func (s *TimelockInspectionTestSuite) TestIsOperationDone() { return isOpDone }, 5*time.Second, 500*time.Millisecond, "Operation was not completed in time") } + +// TestGetMinDelay tests the GetMinDelay method +func (s *TimelockInspectionTestSuite) TestGetMinDelay() { + ctx := context.Background() + inspector := evm.NewTimelockInspector(s.ClientA) + + delay, err := inspector.GetMinDelay(ctx, s.timelockContract.Address().Hex()) + s.Require().NoError(err, "Failed to get min delay") + s.Require().EqualValues(0, delay) +} diff --git a/e2e/tests/solana/timelock_inspection.go b/e2e/tests/solana/timelock_inspection.go index 8d947324..975a6f71 100644 --- a/e2e/tests/solana/timelock_inspection.go +++ b/e2e/tests/solana/timelock_inspection.go @@ -22,6 +22,7 @@ var testPDASeedTimelockIsOperations = [32]byte{'t', 'e', 's', 't', '-', 't', 'i' var testPDASeedTimelockIsOperationsPending = [32]byte{'t', 'e', 's', 't', '-', 't', 'i', 'm', 'e', 'o', 'p', 's', 'p', 'e', 'n', 'd'} var testPDASeedTimelockIsOperationsReady = [32]byte{'t', 'e', 's', 't', '-', 't', 'i', 'm', 'e', 'o', 'p', 's', 'r', 'e', 'a', 'd', 'y'} var testPDASeedTimelockIsOperationsDone = [32]byte{'t', 'e', 's', 't', '-', 't', 'i', 'm', 'e', 'o', 'p', 's', 'd', 'o', 'n', 'e'} +var testPDASeedTimelockMinDelay = [32]byte{'t', 'e', 's', 't', '-', 't', 'i', 'm', 'e', 'm', 'i', 'n', 'd', 'e', 'l', 'a', 'y'} func (s *SolanaTestSuite) TestGetProposers() { s.SetupTimelock(testPDASeedTimelockGetProposers, 1*time.Second) @@ -158,6 +159,18 @@ func (s *SolanaTestSuite) TestIsOperationDone() { s.Require().True(operation, "Operation should be done") } +func (s *SolanaTestSuite) TestGetMinDelay() { + ctx := context.Background() + minDelay := 1 * time.Second + s.SetupTimelock(testPDASeedTimelockMinDelay, minDelay) + + inspector := solanasdk.NewTimelockInspector(s.SolanaClient) + delay, err := inspector.GetMinDelay(ctx, solanasdk.ContractAddress(s.TimelockProgramID, testPDASeedTimelockMinDelay)) + + s.Require().NoError(err, "Failed to query min delay") + s.Require().Equal(delay, uint64(minDelay.Seconds()), "Min delay should match the configured value") +} + func (s *SolanaTestSuite) createOperation(timelockID [32]byte) timelockutils.Operation { salt, serr := timelockutils.SimpleSalt() s.Require().NoError(serr) diff --git a/sdk/aptos/timelock_inspector.go b/sdk/aptos/timelock_inspector.go index 236d5bbf..1bbfb629 100644 --- a/sdk/aptos/timelock_inspector.go +++ b/sdk/aptos/timelock_inspector.go @@ -20,6 +20,16 @@ type TimelockInspector struct { bindingFn func(address aptos.AccountAddress, client aptos.AptosRpcClient) mcms.MCMS } +func (tm TimelockInspector) GetMinDelay(ctx context.Context, address string) (uint64, error) { + mcmsAddress, err := hexToAddress(address) + if err != nil { + return 0, fmt.Errorf("failed to parse MCMS address %q: %w", mcmsAddress, err) + } + mcmsBinding := tm.bindingFn(mcmsAddress, tm.client) + + return mcmsBinding.MCMS().TimelockMinDelay(nil) +} + // NewTimelockInspector creates a new TimelockInspector func NewTimelockInspector(client aptos.AptosRpcClient) *TimelockInspector { return &TimelockInspector{ diff --git a/sdk/aptos/timelock_inspector_test.go b/sdk/aptos/timelock_inspector_test.go index b97650a1..5eaa301d 100644 --- a/sdk/aptos/timelock_inspector_test.go +++ b/sdk/aptos/timelock_inspector_test.go @@ -361,3 +361,84 @@ func TestTimelockInspector_IsOperationDone(t *testing.T) { }) } } + +func TestTimelockInspector_GetMinDelay(t *testing.T) { + t.Parallel() + + type args struct { + mcmsAddr string + } + tests := []struct { + name string + args args + mockSetup func(m *mock_mcms.MCMS) + want uint64 + wantErr assert.ErrorAssertionFunc + }{ + { + name: "success", + args: args{ + mcmsAddr: "0x123", + }, + mockSetup: func(m *mock_mcms.MCMS) { + mockMCMSModule := mock_module_mcms.NewMCMSInterface(t) + m.EXPECT().MCMS().Return(mockMCMSModule) + mockMCMSModule.EXPECT().TimelockMinDelay( + mock.Anything, + ).Return(uint64(321), nil) + }, + want: 321, + wantErr: assert.NoError, + }, + { + name: "failure - invalid MCMS address", + args: args{ + mcmsAddr: "invalidaddress", + }, + wantErr: AssertErrorContains("parse MCMS address"), + }, + { + name: "failure - TimelockMinDelay failed", + args: args{ + mcmsAddr: "0x123", + }, + mockSetup: func(m *mock_mcms.MCMS) { + mockMCMSModule := mock_module_mcms.NewMCMSInterface(t) + m.EXPECT().MCMS().Return(mockMCMSModule) + mockMCMSModule.EXPECT().TimelockMinDelay( + mock.Anything, + ).Return(uint64(0), errors.New("error during TimelockMinDelay")) + }, + want: 0, + wantErr: AssertErrorContains("error during TimelockMinDelay"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mcmsBinding := mock_mcms.NewMCMS(t) + inspector := TimelockInspector{ + bindingFn: func(mcmsAddress aptos.AccountAddress, _ aptos.AptosRpcClient) mcms.MCMS { + // only validate hex address if parsing was supposed to succeed + if tt.name != "failure - invalid MCMS address" { + require.Equal(t, Must(hexToAddress(tt.args.mcmsAddr)), mcmsAddress) + } + + return mcmsBinding + }, + } + + if tt.mockSetup != nil { + tt.mockSetup(mcmsBinding) + } + + got, err := inspector.GetMinDelay(t.Context(), tt.args.mcmsAddr) + if !tt.wantErr(t, err, fmt.Sprintf("GetMinDelay(%q)", tt.args.mcmsAddr)) { + return + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/sdk/evm/timelock_inspector.go b/sdk/evm/timelock_inspector.go index 26674d83..bff04547 100644 --- a/sdk/evm/timelock_inspector.go +++ b/sdk/evm/timelock_inspector.go @@ -142,3 +142,17 @@ func (tm TimelockInspector) IsOperationDone(ctx context.Context, address string, return timelock.IsOperationDone(&bind.CallOpts{Context: ctx}, opID) } + +// GetMinDelay returns the minimum delay for the timelock at the given address +func (tm TimelockInspector) GetMinDelay(ctx context.Context, address string) (uint64, error) { + tl, err := bindings.NewRBACTimelock(common.HexToAddress(address), tm.client) + if err != nil { + return 0, err + } + d, err := tl.GetMinDelay(&bind.CallOpts{Context: ctx}) + if err != nil { + return 0, err + } + + return d.Uint64(), nil +} diff --git a/sdk/evm/timelock_inspector_test.go b/sdk/evm/timelock_inspector_test.go index 7f9a0cbd..0d27dea6 100644 --- a/sdk/evm/timelock_inspector_test.go +++ b/sdk/evm/timelock_inspector_test.go @@ -473,3 +473,75 @@ func TestTimelockInspector_IsOperationDone(t *testing.T) { }) } } + +func TestTimelockInspector_GetMinDelay(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + tests := []struct { + name string + address string + minDelay *big.Int + mockError error + want uint64 + wantErr error + }{ + { + name: "GetMinDelay success", + address: "0x1234567890abcdef1234567890abcdef12345678", + minDelay: big.NewInt(300), + want: 300, + }, + { + name: "GetMinDelay call contract failure error", + address: "0x1234567890abcdef1234567890abcdef12345678", + mockError: errors.New("call to contract failed"), + want: 0, + wantErr: errors.New("call to contract failed"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + // Arrange + mockClient := evm_mocks.NewContractDeployBackend(t) + inspector := NewTimelockInspector(mockClient) + + parsedABI, err := bindings.RBACTimelockMetaData.GetAbi() + require.NoError(t, err) + + if tt.mockError == nil { + // Encode the expected getMinDelay result + encoded, packErr := parsedABI.Methods["getMinDelay"].Outputs.Pack(tt.minDelay) + require.NoError(t, packErr) + + // Expect a single CallContract returning the encoded minDelay + mockClient.EXPECT(). + CallContract(mock.Anything, mock.IsType(ethereum.CallMsg{}), mock.IsType(&big.Int{})). + Return(encoded, nil).Once() + } else { + // Simulate a low-level call failure + mockClient.EXPECT(). + CallContract(mock.Anything, mock.Anything, mock.Anything). + Return(nil, tt.mockError).Once() + } + + // Act + got, err := inspector.GetMinDelay(ctx, tt.address) + + // Assert + if tt.wantErr != nil { + require.Error(t, err) + require.EqualError(t, err, tt.wantErr.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + + mockClient.AssertExpectations(t) + }) + } +} diff --git a/sdk/mocks/timelock_executor.go b/sdk/mocks/timelock_executor.go index 1c3c99f8..50912f88 100644 --- a/sdk/mocks/timelock_executor.go +++ b/sdk/mocks/timelock_executor.go @@ -262,6 +262,63 @@ func (_c *TimelockExecutor_GetExecutors_Call) RunAndReturn(run func(context.Cont return _c } +// GetMinDelay provides a mock function with given fields: ctx, address +func (_m *TimelockExecutor) GetMinDelay(ctx context.Context, address string) (uint64, error) { + ret := _m.Called(ctx, address) + + if len(ret) == 0 { + panic("no return value specified for GetMinDelay") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (uint64, error)); ok { + return rf(ctx, address) + } + if rf, ok := ret.Get(0).(func(context.Context, string) uint64); ok { + r0 = rf(ctx, address) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, address) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TimelockExecutor_GetMinDelay_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMinDelay' +type TimelockExecutor_GetMinDelay_Call struct { + *mock.Call +} + +// GetMinDelay is a helper method to define mock.On call +// - ctx context.Context +// - address string +func (_e *TimelockExecutor_Expecter) GetMinDelay(ctx interface{}, address interface{}) *TimelockExecutor_GetMinDelay_Call { + return &TimelockExecutor_GetMinDelay_Call{Call: _e.mock.On("GetMinDelay", ctx, address)} +} + +func (_c *TimelockExecutor_GetMinDelay_Call) Run(run func(ctx context.Context, address string)) *TimelockExecutor_GetMinDelay_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *TimelockExecutor_GetMinDelay_Call) Return(_a0 uint64, _a1 error) *TimelockExecutor_GetMinDelay_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TimelockExecutor_GetMinDelay_Call) RunAndReturn(run func(context.Context, string) (uint64, error)) *TimelockExecutor_GetMinDelay_Call { + _c.Call.Return(run) + return _c +} + // GetProposers provides a mock function with given fields: ctx, address func (_m *TimelockExecutor) GetProposers(ctx context.Context, address string) ([]string, error) { ret := _m.Called(ctx, address) diff --git a/sdk/mocks/timelock_inspector.go b/sdk/mocks/timelock_inspector.go index 657e87ad..e2e023f6 100644 --- a/sdk/mocks/timelock_inspector.go +++ b/sdk/mocks/timelock_inspector.go @@ -198,6 +198,63 @@ func (_c *TimelockInspector_GetExecutors_Call) RunAndReturn(run func(context.Con return _c } +// GetMinDelay provides a mock function with given fields: ctx, address +func (_m *TimelockInspector) GetMinDelay(ctx context.Context, address string) (uint64, error) { + ret := _m.Called(ctx, address) + + if len(ret) == 0 { + panic("no return value specified for GetMinDelay") + } + + var r0 uint64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (uint64, error)); ok { + return rf(ctx, address) + } + if rf, ok := ret.Get(0).(func(context.Context, string) uint64); ok { + r0 = rf(ctx, address) + } else { + r0 = ret.Get(0).(uint64) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, address) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TimelockInspector_GetMinDelay_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetMinDelay' +type TimelockInspector_GetMinDelay_Call struct { + *mock.Call +} + +// GetMinDelay is a helper method to define mock.On call +// - ctx context.Context +// - address string +func (_e *TimelockInspector_Expecter) GetMinDelay(ctx interface{}, address interface{}) *TimelockInspector_GetMinDelay_Call { + return &TimelockInspector_GetMinDelay_Call{Call: _e.mock.On("GetMinDelay", ctx, address)} +} + +func (_c *TimelockInspector_GetMinDelay_Call) Run(run func(ctx context.Context, address string)) *TimelockInspector_GetMinDelay_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *TimelockInspector_GetMinDelay_Call) Return(_a0 uint64, _a1 error) *TimelockInspector_GetMinDelay_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TimelockInspector_GetMinDelay_Call) RunAndReturn(run func(context.Context, string) (uint64, error)) *TimelockInspector_GetMinDelay_Call { + _c.Call.Return(run) + return _c +} + // GetProposers provides a mock function with given fields: ctx, address func (_m *TimelockInspector) GetProposers(ctx context.Context, address string) ([]string, error) { ret := _m.Called(ctx, address) diff --git a/sdk/solana/timelock_inspector.go b/sdk/solana/timelock_inspector.go index 31436b2e..918f9363 100644 --- a/sdk/solana/timelock_inspector.go +++ b/sdk/solana/timelock_inspector.go @@ -128,6 +128,25 @@ func (t TimelockInspector) IsOperationDone(ctx context.Context, address string, return op.State == timelock.Done_OperationState, nil } +func (t TimelockInspector) GetMinDelay(ctx context.Context, address string) (uint64, error) { + programID, seed, err := ParseContractAddress(address) + if err != nil { + return 0, err + } + + pda, err := FindTimelockConfigPDA(programID, seed) + if err != nil { + return 0, err + } + + configAccount, err := GetTimelockConfig(ctx, t.client, pda) + if err != nil { + return 0, err + } + + return configAccount.MinDelay, nil +} + func (t TimelockInspector) getOpData(ctx context.Context, address string, opID [32]byte) (timelock.Operation, error) { programID, seed, err := ParseContractAddress(address) if err != nil { diff --git a/sdk/solana/timelock_inspector_test.go b/sdk/solana/timelock_inspector_test.go index a7707050..a7f99917 100644 --- a/sdk/solana/timelock_inspector_test.go +++ b/sdk/solana/timelock_inspector_test.go @@ -567,6 +567,54 @@ func TestTimelockInspector_getRoleAccessController(t *testing.T) { } } +func TestTimelockInspector_GetMinDelay(t *testing.T) { + t.Parallel() + + timelockConfigPDA, err := FindTimelockConfigPDA(testTimelockProgramID, testPDASeed) + require.NoError(t, err) + + cfg := createTimelockConfig(t) + cfg.MinDelay = 123 + + tests := []struct { + name string + setup func(*mocks.JSONRPCClient) + want uint64 + wantErr string + }{ + { + name: "success", + setup: func(mockJSONRPCClient *mocks.JSONRPCClient) { + mockGetAccountInfo(t, mockJSONRPCClient, timelockConfigPDA, cfg, nil) + }, + want: 123, + }, + { + name: "error: get timelock config account info rpc error", + setup: func(mockJSONRPCClient *mocks.JSONRPCClient) { + mockGetAccountInfo(t, mockJSONRPCClient, timelockConfigPDA, cfg, errors.New("rpc error")) + }, + wantErr: "rpc error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + inspector, jsonRPCClient := newTestTimelockInspector(t) + tt.setup(jsonRPCClient) + + got, err := inspector.GetMinDelay(context.Background(), ContractAddress(testTimelockProgramID, testPDASeed)) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} + // ----- helpers ----- func newTestTimelockInspector(t *testing.T) (*TimelockInspector, *mocks.JSONRPCClient) { diff --git a/sdk/timelock_inspector.go b/sdk/timelock_inspector.go index 2b68f46a..3094b5f6 100644 --- a/sdk/timelock_inspector.go +++ b/sdk/timelock_inspector.go @@ -13,4 +13,5 @@ type TimelockInspector interface { IsOperationPending(ctx context.Context, address string, opID [32]byte) (bool, error) IsOperationReady(ctx context.Context, address string, opID [32]byte) (bool, error) IsOperationDone(ctx context.Context, address string, opID [32]byte) (bool, error) + GetMinDelay(ctx context.Context, address string) (uint64, error) }