diff --git a/x/budget/keeper/budget.go b/x/budget/keeper/budget.go index 70116c3..a75eb81 100644 --- a/x/budget/keeper/budget.go +++ b/x/budget/keeper/budget.go @@ -13,7 +13,11 @@ import ( // CollectBudgets collects all the valid budgets registered in params.Budgets and // distributes the total collected coins to collection address. func (k Keeper) CollectBudgets(ctx sdk.Context) error { - budgets := k.CollectibleBudgets(ctx) + params := k.GetParams(ctx) + var budgets []types.Budget + if params.EpochBlocks > 0 && ctx.BlockHeight()%int64(params.EpochBlocks) == 0 { + budgets = types.CollectibleBudgets(params.Budgets, ctx.BlockTime()) + } if len(budgets) == 0 { return nil } @@ -90,21 +94,6 @@ func (k Keeper) CollectBudgets(ctx sdk.Context) error { return nil } -// CollectibleBudgets returns scan through the budgets registered in params.Budgets -// and returns only the valid and not expired budgets. -func (k Keeper) CollectibleBudgets(ctx sdk.Context) (budgets []types.Budget) { - params := k.GetParams(ctx) - if params.EpochBlocks > 0 && ctx.BlockHeight()%int64(params.EpochBlocks) == 0 { - for _, budget := range params.Budgets { - err := budget.Validate() - if err == nil && budget.Collectible(ctx.BlockTime()) { - budgets = append(budgets, budget) - } - } - } - return -} - // GetTotalCollectedCoins returns total collected coins for a budget. func (k Keeper) GetTotalCollectedCoins(ctx sdk.Context, budgetName string) sdk.Coins { store := ctx.KVStore(k.storeKey) diff --git a/x/budget/keeper/budget_test.go b/x/budget/keeper/budget_test.go index 7563861..ddd741f 100644 --- a/x/budget/keeper/budget_test.go +++ b/x/budget/keeper/budget_test.go @@ -184,7 +184,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { params := suite.keeper.GetParams(suite.ctx) suite.keeper.SetParams(suite.ctx, params) height := 1 - suite.ctx = suite.ctx.WithBlockTime(mustParseRFC3339("2021-08-01T00:00:00Z")) + suite.ctx = suite.ctx.WithBlockTime(types.MustParseRFC3339("2021-08-01T00:00:00Z")) suite.ctx = suite.ctx.WithBlockHeight(int64(height)) for _, tc := range []struct { @@ -196,7 +196,6 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { nextBlockTime time.Time expErr error }{ - { "add budget 1", testProposal(proposal.ParamChange{ @@ -215,8 +214,8 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { }), 1, 0, - mustParseRFC3339("2021-08-01T00:00:00Z"), - mustParseRFC3339("2021-08-01T00:00:00Z"), + types.MustParseRFC3339("2021-08-01T00:00:00Z"), + types.MustParseRFC3339("2021-08-01T00:00:00Z"), nil, }, { @@ -245,8 +244,8 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { }), 2, 2, - mustParseRFC3339("2021-09-03T00:00:00Z"), - mustParseRFC3339("2021-09-03T00:00:00Z"), + types.MustParseRFC3339("2021-09-03T00:00:00Z"), + types.MustParseRFC3339("2021-09-03T00:00:00Z"), nil, }, { @@ -283,8 +282,8 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { }), 0, 0, - mustParseRFC3339("2021-09-29T00:00:00Z"), - mustParseRFC3339("2021-09-30T00:00:00Z"), + types.MustParseRFC3339("2021-09-29T00:00:00Z"), + types.MustParseRFC3339("2021-09-30T00:00:00Z"), types.ErrInvalidTotalBudgetRate, }, { @@ -321,8 +320,8 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { }), 0, 0, - mustParseRFC3339("2021-10-01T00:00:00Z"), - mustParseRFC3339("2021-10-01T00:00:00Z"), + types.MustParseRFC3339("2021-10-01T00:00:00Z"), + types.MustParseRFC3339("2021-10-01T00:00:00Z"), types.ErrInvalidTotalBudgetRate, }, { @@ -351,8 +350,38 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { }), 2, 2, - mustParseRFC3339("2021-10-01T00:00:00Z"), - mustParseRFC3339("2021-10-01T00:00:00Z"), + types.MustParseRFC3339("2021-10-01T00:00:00Z"), + types.MustParseRFC3339("2021-10-01T00:00:00Z"), + nil, + }, + { + "add budget 4 without date range overlap", + testProposal(proposal.ParamChange{ + Subspace: types.ModuleName, + Key: string(types.KeyBudgets), + Value: `[ + { + "name": "gravity-dex-farming-20213Q-20313Q", + "rate": "0.500000000000000000", + "budget_source_address": "cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta", + "collection_address": "cosmos1228ryjucdpdv3t87rxle0ew76a56ulvnfst0hq0sscd3nafgjpqqkcxcky", + "start_time": "2021-09-01T00:00:00Z", + "end_time": "2031-09-30T00:00:00Z" + }, + { + "name": "gravity-dex-farming-4", + "rate": "1.000000000000000000", + "budget_source_address": "cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta", + "collection_address": "cosmos17avp6xs5c8ycqzy20yv99ccxwunu32e507kpm8ql5nfg47pzj9qqxhujxr", + "start_time": "2031-09-30T00:00:01Z", + "end_time": "2031-12-10T00:00:00Z" + } + ]`, + }), + 2, + 1, + types.MustParseRFC3339("2021-09-29T00:00:00Z"), + types.MustParseRFC3339("2021-09-30T00:00:00Z"), nil, }, } { @@ -384,7 +413,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { height += 1 suite.ctx = suite.ctx.WithBlockHeight(int64(height)) suite.ctx = suite.ctx.WithBlockTime(tc.nextBlockTime) - budgets := suite.keeper.CollectibleBudgets(suite.ctx) + budgets := types.CollectibleBudgets(params.Budgets, suite.ctx.BlockTime()) suite.Require().Len(budgets, tc.collectibleBudgetCount) // BeginBlocker @@ -418,8 +447,8 @@ func (suite *KeeperTestSuite) TestTotalCollectedCoins() { Rate: sdk.NewDecWithPrec(5, 2), // 5% BudgetSourceAddress: suite.budgetSourceAddrs[0].String(), CollectionAddress: suite.collectionAddrs[0].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), } params := suite.keeper.GetParams(suite.ctx) @@ -432,7 +461,7 @@ func (suite *KeeperTestSuite) TestTotalCollectedCoins() { collectedCoins := suite.keeper.GetTotalCollectedCoins(suite.ctx, "budget1") suite.Require().Equal(sdk.Coins(nil), collectedCoins) - suite.ctx = suite.ctx.WithBlockTime(mustParseRFC3339("2021-08-31T00:00:00Z")) + suite.ctx = suite.ctx.WithBlockTime(types.MustParseRFC3339("2021-08-31T00:00:00Z")) err := suite.keeper.CollectBudgets(suite.ctx) suite.Require().NoError(err) diff --git a/x/budget/keeper/grpc_query_test.go b/x/budget/keeper/grpc_query_test.go index 68c2cc3..5fcd39a 100644 --- a/x/budget/keeper/grpc_query_test.go +++ b/x/budget/keeper/grpc_query_test.go @@ -21,32 +21,32 @@ func (suite *KeeperTestSuite) TestGRPCBudgets() { Rate: sdk.NewDecWithPrec(5, 2), BudgetSourceAddress: suite.budgetSourceAddrs[0].String(), CollectionAddress: suite.collectionAddrs[0].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, { Name: "budget2", Rate: sdk.NewDecWithPrec(5, 2), BudgetSourceAddress: suite.budgetSourceAddrs[0].String(), CollectionAddress: suite.collectionAddrs[1].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, { Name: "budget3", Rate: sdk.NewDecWithPrec(5, 2), BudgetSourceAddress: suite.budgetSourceAddrs[1].String(), CollectionAddress: suite.collectionAddrs[0].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, { Name: "budget4", Rate: sdk.NewDecWithPrec(5, 2), BudgetSourceAddress: suite.budgetSourceAddrs[1].String(), CollectionAddress: suite.collectionAddrs[1].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, } @@ -57,7 +57,7 @@ func (suite *KeeperTestSuite) TestGRPCBudgets() { balance := suite.app.BankKeeper.GetAllBalances(suite.ctx, suite.budgetSourceAddrs[0]) expectedCoins, _ := sdk.NewDecCoinsFromCoins(balance...).MulDec(sdk.NewDecWithPrec(5, 2)).TruncateDecimal() - suite.ctx = suite.ctx.WithBlockTime(mustParseRFC3339("2021-08-31T00:00:00Z")) + suite.ctx = suite.ctx.WithBlockTime(types.MustParseRFC3339("2021-08-31T00:00:00Z")) err := suite.keeper.CollectBudgets(suite.ctx) suite.Require().NoError(err) diff --git a/x/budget/keeper/keeper_test.go b/x/budget/keeper/keeper_test.go index 32a9dea..b95e29b 100644 --- a/x/budget/keeper/keeper_test.go +++ b/x/budget/keeper/keeper_test.go @@ -2,7 +2,6 @@ package keeper_test import ( "testing" - "time" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" @@ -91,56 +90,56 @@ func (suite *KeeperTestSuite) SetupTest() { Rate: sdk.MustNewDecFromStr("0.5"), BudgetSourceAddress: suite.budgetSourceAddrs[0].String(), CollectionAddress: suite.collectionAddrs[0].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, { Name: "budget2", Rate: sdk.MustNewDecFromStr("0.5"), BudgetSourceAddress: suite.budgetSourceAddrs[0].String(), CollectionAddress: suite.collectionAddrs[1].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, { Name: "budget3", Rate: sdk.MustNewDecFromStr("1.0"), BudgetSourceAddress: suite.budgetSourceAddrs[1].String(), CollectionAddress: suite.collectionAddrs[2].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, { Name: "budget4", Rate: sdk.MustNewDecFromStr("1"), BudgetSourceAddress: suite.budgetSourceAddrs[2].String(), CollectionAddress: suite.collectionAddrs[3].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("0000-01-01T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("0000-01-02T00:00:00Z"), }, { Name: "budget5", Rate: sdk.MustNewDecFromStr("0.5"), BudgetSourceAddress: suite.budgetSourceAddrs[3].String(), CollectionAddress: suite.collectionAddrs[0].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, { Name: "budget6", Rate: sdk.MustNewDecFromStr("0.5"), BudgetSourceAddress: suite.budgetSourceAddrs[3].String(), CollectionAddress: suite.collectionAddrs[1].String(), - StartTime: mustParseRFC3339("0000-01-01T00:00:00Z"), - EndTime: mustParseRFC3339("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, { Name: "gravity-dex-farming-20213Q-20313Q", Rate: sdk.MustNewDecFromStr("0.5"), BudgetSourceAddress: suite.budgetSourceAddrs[5].String(), CollectionAddress: suite.collectionAddrs[5].String(), - StartTime: mustParseRFC3339("2021-09-01T00:00:00Z"), - EndTime: mustParseRFC3339("2031-09-30T00:00:00Z"), + StartTime: types.MustParseRFC3339("2021-09-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2031-09-30T00:00:00Z"), }, } } @@ -149,14 +148,6 @@ func coinsEq(exp, got sdk.Coins) (bool, string, string, string) { return exp.IsEqual(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String() } -func mustParseRFC3339(s string) time.Time { - t, err := time.Parse(time.RFC3339, s) - if err != nil { - panic(err) - } - return t -} - func mustParseCoinsNormalized(coinStr string) (coins sdk.Coins) { coins, err := sdk.ParseCoinsNormalized(coinStr) if err != nil { diff --git a/x/budget/simulation/genesis.go b/x/budget/simulation/genesis.go index 5317f87..a6d2a33 100644 --- a/x/budget/simulation/genesis.go +++ b/x/budget/simulation/genesis.go @@ -36,8 +36,8 @@ func GenBudgets(r *rand.Rand) []types.Budget { Rate: sdk.NewDecFromIntWithPrec(sdk.NewInt(int64(simtypes.RandIntBetween(r, 1, 4))), 1), // 10~30% BudgetSourceAddress: "cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta", // Cosmos Hub's FeeCollector module account CollectionAddress: sdk.AccAddress(address.Module(types.ModuleName, []byte("GravityDEXFarmingBudget"))).String(), - StartTime: types.ParseTime("2000-01-01T00:00:00Z"), - EndTime: types.ParseTime("9999-12-31T00:00:00Z"), + StartTime: types.MustParseRFC3339("2000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), } ranBudgets = append(ranBudgets, budget) } diff --git a/x/budget/types/budget.go b/x/budget/types/budget.go index f22ddf8..dbf70be 100644 --- a/x/budget/types/budget.go +++ b/x/budget/types/budget.go @@ -45,7 +45,7 @@ func (budget Budget) Validate() error { return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid budget source address %s: %v", budget.BudgetSourceAddress, err) } - if budget.EndTime.Before(budget.StartTime) { + if !budget.EndTime.After(budget.StartTime) { return ErrInvalidStartEndTime } @@ -63,6 +63,16 @@ func (budget Budget) Collectible(blockTime time.Time) bool { return !budget.StartTime.After(blockTime) && budget.EndTime.After(blockTime) } +// CollectibleBudgets returns only the valid and started and not expired budgets based on the given block time. +func CollectibleBudgets(budgets []Budget, blockTime time.Time) (collectibleBudgets []Budget) { + for _, budget := range budgets { + if budget.Collectible(blockTime) { + collectibleBudgets = append(collectibleBudgets, budget) + } + } + return +} + // ValidateName is the default validation function for Budget.Name. // A budget name only allows alphabet letters(`A-Z, a-z`), digit numbers(`0-9`), and `-`. // It doesn't allow spaces and the maximum length is 50 characters. diff --git a/x/budget/types/errors.go b/x/budget/types/errors.go index e96cad5..e556985 100644 --- a/x/budget/types/errors.go +++ b/x/budget/types/errors.go @@ -7,7 +7,7 @@ import ( // Sentinel errors for the budget module. var ( ErrInvalidBudgetName = sdkerrors.Register(ModuleName, 2, "budget name only allows letters, digits, and dash(-) without spaces and the maximum length is 50") - ErrInvalidStartEndTime = sdkerrors.Register(ModuleName, 3, "budget end time should not be earlier than budget start time") + ErrInvalidStartEndTime = sdkerrors.Register(ModuleName, 3, "budget end time must be after the start time") ErrInvalidBudgetRate = sdkerrors.Register(ModuleName, 4, "invalid budget rate") ErrInvalidTotalBudgetRate = sdkerrors.Register(ModuleName, 5, "invalid total rate of the budgets with the same budget source address") ErrDuplicateBudgetName = sdkerrors.Register(ModuleName, 6, "duplicate budget name") diff --git a/x/budget/types/params.go b/x/budget/types/params.go index 14caa8e..d1aece8 100644 --- a/x/budget/types/params.go +++ b/x/budget/types/params.go @@ -85,13 +85,25 @@ func ValidateBudgets(i interface{}) error { } names[budget.Name] = true } - budgetsBySourceMap := GetBudgetsBySourceMap(budgets) - for addr, budgets := range budgetsBySourceMap { - if budgets.TotalRate.GT(sdk.OneDec()) { - return sdkerrors.Wrapf( - ErrInvalidTotalBudgetRate, - "total rate for budget source address %s must not exceed 1: %v", addr, budgets.TotalRate) + for addr, budgetsBySource := range budgetsBySourceMap { + if budgetsBySource.TotalRate.GT(sdk.OneDec()) { + // If the TotalRate of Budgets with the same BudgetSourceAddress exceeds 1, + // recalculate and verify the TotalRate of Budgets with overlapping time ranges. + for _, budget := range budgetsBySource.Budgets { + totalRate := sdk.ZeroDec() + for _, budgetToCheck := range budgetsBySource.Budgets { + if DateRangesOverlap(budget.StartTime, budget.EndTime, budgetToCheck.StartTime, budgetToCheck.EndTime) { + totalRate = totalRate.Add(budgetToCheck.Rate) + } + } + if totalRate.GT(sdk.OneDec()) { + return sdkerrors.Wrapf( + ErrInvalidTotalBudgetRate, + "total rate for budget source address %s must not exceed 1: %v", addr, totalRate) + } + } + } } return nil diff --git a/x/budget/types/params_test.go b/x/budget/types/params_test.go index 9505101..653eebe 100644 --- a/x/budget/types/params_test.go +++ b/x/budget/types/params_test.go @@ -2,7 +2,6 @@ package types_test import ( "testing" - "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/address" @@ -12,67 +11,108 @@ import ( "github.com/tendermint/budget/x/budget/types" ) -func TestParams(t *testing.T) { - require.IsType(t, paramstypes.KeyTable{}, types.ParamKeyTable()) - - defaultParams := types.DefaultParams() - - paramsStr := `epoch_blocks: 1 -budgets: [] -` - require.Equal(t, paramsStr, defaultParams.String()) -} - -func TestValidateBudgets(t *testing.T) { - cAddr1 := sdk.AccAddress(address.Module(types.ModuleName, []byte("collectionAddr1"))) - cAddr2 := sdk.AccAddress(address.Module(types.ModuleName, []byte("collectionAddr2"))) - tAddr1 := sdk.AccAddress(address.Module(types.ModuleName, []byte("budgetSourceAddr1"))) - tAddr2 := sdk.AccAddress(address.Module(types.ModuleName, []byte("budgetSourceAddr2"))) - budgets := []types.Budget{ +var ( + cAddr1 = sdk.AccAddress(address.Module(types.ModuleName, []byte("collectionAddr1"))) + cAddr2 = sdk.AccAddress(address.Module(types.ModuleName, []byte("collectionAddr2"))) + tAddr1 = sdk.AccAddress(address.Module(types.ModuleName, []byte("budgetSourceAddr1"))) + tAddr2 = sdk.AccAddress(address.Module(types.ModuleName, []byte("budgetSourceAddr2"))) + budgets = []types.Budget{ { Name: "test", - Rate: sdk.NewDec(1), + Rate: sdk.OneDec(), BudgetSourceAddress: tAddr1.String(), CollectionAddress: cAddr1.String(), - StartTime: time.Time{}, - EndTime: time.Time{}, + StartTime: types.MustParseRFC3339("2021-08-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-03T00:00:00Z"), + }, + { + Name: "test1", + Rate: sdk.OneDec(), + BudgetSourceAddress: tAddr2.String(), + CollectionAddress: cAddr2.String(), + StartTime: types.MustParseRFC3339("2021-07-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-07-10T00:00:00Z"), }, { Name: "test2", - Rate: sdk.NewDec(1), + Rate: sdk.MustNewDecFromStr("0.1"), BudgetSourceAddress: tAddr2.String(), CollectionAddress: cAddr2.String(), - StartTime: time.Time{}, - EndTime: time.Time{}, + StartTime: types.MustParseRFC3339("2021-07-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-07-10T00:00:00Z"), }, { Name: "test3", Rate: sdk.MustNewDecFromStr("0.1"), BudgetSourceAddress: tAddr2.String(), CollectionAddress: cAddr2.String(), - StartTime: time.Time{}, - EndTime: time.Time{}, + StartTime: types.MustParseRFC3339("2021-08-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-10T00:00:00Z"), }, { - Name: "test3", + Name: "test4", + Rate: sdk.OneDec(), + BudgetSourceAddress: tAddr2.String(), + CollectionAddress: cAddr2.String(), + StartTime: types.MustParseRFC3339("2021-08-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-20T00:00:00Z"), + }, + { + Name: "test5", Rate: sdk.MustNewDecFromStr("0.1"), BudgetSourceAddress: tAddr2.String(), CollectionAddress: cAddr2.String(), - StartTime: time.Time{}, - EndTime: time.Time{}, + StartTime: types.MustParseRFC3339("2021-08-19T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-25T00:00:00Z"), }, } +) - err := types.ValidateBudgets(budgets[:2]) +func TestParams(t *testing.T) { + require.IsType(t, paramstypes.KeyTable{}, types.ParamKeyTable()) + + defaultParams := types.DefaultParams() + + paramsStr := `epoch_blocks: 1 +budgets: [] +` + require.Equal(t, paramsStr, defaultParams.String()) +} + +func TestValidateBudgets(t *testing.T) { + err := types.ValidateBudgets([]types.Budget{budgets[0], budgets[1]}) + require.NoError(t, err) + + err = types.ValidateBudgets([]types.Budget{budgets[0], budgets[1], budgets[2]}) + require.ErrorIs(t, err, types.ErrInvalidTotalBudgetRate) + + err = types.ValidateBudgets([]types.Budget{budgets[1], budgets[4]}) require.NoError(t, err) - err = types.ValidateBudgets(budgets[:3]) + err = types.ValidateBudgets([]types.Budget{budgets[4], budgets[5]}) require.ErrorIs(t, err, types.ErrInvalidTotalBudgetRate) - err = types.ValidateBudgets(budgets) + err = types.ValidateBudgets([]types.Budget{budgets[3], budgets[3]}) require.ErrorIs(t, err, types.ErrDuplicateBudgetName) } +func TestCollectibleBudgets(t *testing.T) { + collectibleBudgets := types.CollectibleBudgets([]types.Budget{budgets[0], budgets[1]}, types.MustParseRFC3339("2021-07-05T00:00:00Z")) + require.Len(t, collectibleBudgets, 1) + + collectibleBudgets = types.CollectibleBudgets([]types.Budget{budgets[0], budgets[1], budgets[2]}, types.MustParseRFC3339("2021-07-05T00:00:00Z")) + require.Len(t, collectibleBudgets, 2) + + collectibleBudgets = types.CollectibleBudgets([]types.Budget{budgets[4], budgets[5]}, types.MustParseRFC3339("2021-08-18T00:00:00Z")) + require.Len(t, collectibleBudgets, 1) + + collectibleBudgets = types.CollectibleBudgets([]types.Budget{budgets[4], budgets[5]}, types.MustParseRFC3339("2021-08-19T00:00:00Z")) + require.Len(t, collectibleBudgets, 2) + + collectibleBudgets = types.CollectibleBudgets([]types.Budget{budgets[4], budgets[5]}, types.MustParseRFC3339("2021-08-20T00:00:00Z")) + require.Len(t, collectibleBudgets, 1) +} + func TestValidateEpochBlocks(t *testing.T) { err := types.ValidateEpochBlocks(uint32(0)) require.NoError(t, err) diff --git a/x/budget/types/utils.go b/x/budget/types/utils.go index f660741..82a8e28 100644 --- a/x/budget/types/utils.go +++ b/x/budget/types/utils.go @@ -1,12 +1,20 @@ package types -import "time" +import ( + "time" +) -// ParseTime parses string time to time in RFC3339 format. -func ParseTime(s string) time.Time { +// MustParseRFC3339 parses string time to time in RFC3339 format. +func MustParseRFC3339(s string) time.Time { t, err := time.Parse(time.RFC3339, s) if err != nil { panic(err) } return t } + +// DateRangesOverlap returns true if two date ranges overlap each other. +// End time is exclusive and start time is inclusive. +func DateRangesOverlap(startTimeA, endTimeA, startTimeB, endTimeB time.Time) bool { + return startTimeA.Before(endTimeB) && endTimeA.After(startTimeB) +} diff --git a/x/budget/types/utils_test.go b/x/budget/types/utils_test.go index b8bce23..d86bdbe 100644 --- a/x/budget/types/utils_test.go +++ b/x/budget/types/utils_test.go @@ -15,6 +15,72 @@ func TestParseTime(t *testing.T) { require.NoError(t, err) errorCase := "9999-12-31T00:00:00_ErrorCase" _, err = time.Parse(time.RFC3339, errorCase) - require.PanicsWithError(t, err.Error(), func() { types.ParseTime(errorCase) }) - require.Equal(t, normalRes, types.ParseTime(normalCase)) + require.PanicsWithError(t, err.Error(), func() { types.MustParseRFC3339(errorCase) }) + require.Equal(t, normalRes, types.MustParseRFC3339(normalCase)) +} + +func TestDateRangesOverlap(t *testing.T) { + testCases := []struct { + name string + expectedResult bool + startTimeA time.Time + endTimeA time.Time + startTimeB time.Time + endTimeB time.Time + }{ + { + "not overlapping", + false, + types.MustParseRFC3339("2021-12-01T00:00:00Z"), + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-03T00:00:00Z"), + types.MustParseRFC3339("2021-12-04T00:00:00Z"), + }, + { + "same end time and start time", + false, + types.MustParseRFC3339("2021-12-01T00:00:00Z"), + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-03T00:00:00Z"), + }, + { + "end time and start time differs by a little amount", + true, + types.MustParseRFC3339("2021-12-01T00:00:00Z"), + types.MustParseRFC3339("2021-12-02T00:00:00.001Z"), + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-03T00:00:00Z"), + }, + { + "overlap #1", + true, + types.MustParseRFC3339("2021-12-01T00:00:00Z"), + types.MustParseRFC3339("2021-12-03T00:00:00Z"), + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-04T00:00:00Z"), + }, + { + "overlap #2 - same ranges", + true, + types.MustParseRFC3339("2021-12-01T00:00:00Z"), + types.MustParseRFC3339("2021-12-03T00:00:00Z"), + types.MustParseRFC3339("2021-12-01T00:00:00Z"), + types.MustParseRFC3339("2021-12-03T00:00:00Z"), + }, + { + "overlap #3 - one includes another", + true, + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-03T00:00:00Z"), + types.MustParseRFC3339("2021-12-01T00:00:00Z"), + types.MustParseRFC3339("2021-12-04T00:00:00Z"), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expectedResult, types.DateRangesOverlap(tc.startTimeA, tc.endTimeA, tc.startTimeB, tc.endTimeB)) + require.Equal(t, tc.expectedResult, types.DateRangesOverlap(tc.startTimeB, tc.endTimeB, tc.startTimeA, tc.endTimeA)) + }) + } }