package keeper_test import ( "time" "cosmossdk.io/core/header" "cosmossdk.io/x/protocolpool/types" sdk "github.com/cosmos/cosmos-sdk/types" ) var ( recipientAddr = sdk.AccAddress([]byte("to1__________________")) fooCoin = sdk.NewInt64Coin("foo", 100) fooCoin2 = sdk.NewInt64Coin("foo", 50) ) func (suite *KeeperTestSuite) TestMsgSubmitBudgetProposal() { invalidCoin := sdk.NewInt64Coin("foo", 0) startTime := suite.ctx.BlockTime().Add(10 * time.Second) invalidStartTime := suite.ctx.BlockTime().Add(-15 * time.Second) period := time.Duration(60) * time.Second zeroPeriod := time.Duration(0) * time.Second testCases := map[string]struct { preRun func() input *types.MsgSubmitBudgetProposal expErr bool expErrMsg string }{ "empty recipient address": { input: &types.MsgSubmitBudgetProposal{ Authority: suite.poolKeeper.GetAuthority(), RecipientAddress: "", TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 2, Period: &period, }, expErr: true, expErrMsg: "empty address string is not allowed", }, "empty authority": { input: &types.MsgSubmitBudgetProposal{ Authority: "", RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 2, Period: &period, }, expErr: true, expErrMsg: "empty address string is not allowed", }, "invalid authority": { input: &types.MsgSubmitBudgetProposal{ Authority: "invalid_authority", RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 2, Period: &period, }, expErr: true, expErrMsg: "invalid authority", }, "invalid budget": { input: &types.MsgSubmitBudgetProposal{ Authority: suite.poolKeeper.GetAuthority(), RecipientAddress: recipientAddr.String(), TotalBudget: &invalidCoin, StartTime: &startTime, Tranches: 2, Period: &period, }, expErr: true, expErrMsg: "total budget cannot be zero", }, "invalid start time": { input: &types.MsgSubmitBudgetProposal{ Authority: suite.poolKeeper.GetAuthority(), RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &invalidStartTime, Tranches: 2, Period: &period, }, expErr: true, expErrMsg: "start time cannot be less than the current block time", }, "invalid tranches": { input: &types.MsgSubmitBudgetProposal{ Authority: suite.poolKeeper.GetAuthority(), RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 0, Period: &period, }, expErr: true, expErrMsg: "tranches must be greater than zero", }, "invalid period": { input: &types.MsgSubmitBudgetProposal{ Authority: suite.poolKeeper.GetAuthority(), RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 2, Period: &zeroPeriod, }, expErr: true, expErrMsg: "period length should be greater than zero", }, "all good": { input: &types.MsgSubmitBudgetProposal{ Authority: suite.poolKeeper.GetAuthority(), RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 2, Period: &period, }, expErr: false, }, } for name, tc := range testCases { suite.Run(name, func() { suite.SetupTest() if tc.preRun != nil { tc.preRun() } _, err := suite.msgServer.SubmitBudgetProposal(suite.ctx, tc.input) if tc.expErr { suite.Require().Error(err) suite.Require().Contains(err.Error(), tc.expErrMsg) } else { suite.Require().NoError(err) } }) } } func (suite *KeeperTestSuite) TestMsgClaimBudget() { startTime := suite.ctx.BlockTime().Add(-70 * time.Second) period := time.Duration(60) * time.Second testCases := map[string]struct { preRun func() recipientAddress sdk.AccAddress expErr bool expErrMsg string claimableFunds sdk.Coin }{ "empty recipient addr": { recipientAddress: sdk.AccAddress(""), expErr: true, expErrMsg: "invalid recipient address: empty address string is not allowed", }, "no budget found": { recipientAddress: sdk.AccAddress([]byte("acc1__________")), expErr: true, expErrMsg: "no budget found for recipient", }, "claiming before start time": { preRun: func() { startTime := suite.ctx.BlockTime().Add(3600 * time.Second) // Prepare the budget proposal with a future start time budget := types.Budget{ RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 2, Period: &period, } err := suite.poolKeeper.BudgetProposal.Set(suite.ctx, recipientAddr, budget) suite.Require().NoError(err) }, recipientAddress: recipientAddr, expErr: true, expErrMsg: "distribution has not started yet", }, "budget period has not passed": { preRun: func() { startTime := suite.ctx.BlockTime().Add(-50 * time.Second) // Prepare the budget proposal with start time and a short period budget := types.Budget{ RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 1, Period: &period, } err := suite.poolKeeper.BudgetProposal.Set(suite.ctx, recipientAddr, budget) suite.Require().NoError(err) }, recipientAddress: recipientAddr, expErr: true, expErrMsg: "budget period has not passed yet", }, "valid claim": { preRun: func() { // Prepare the budget proposal with valid start time and period budget := types.Budget{ RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 2, Period: &period, } err := suite.poolKeeper.BudgetProposal.Set(suite.ctx, recipientAddr, budget) suite.Require().NoError(err) }, recipientAddress: recipientAddr, expErr: false, claimableFunds: sdk.NewInt64Coin("foo", 50), }, "double claim attempt with budget period not passed": { preRun: func() { // Prepare the budget proposal with valid start time and period budget := types.Budget{ RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 2, Period: &period, } err := suite.poolKeeper.BudgetProposal.Set(suite.ctx, recipientAddr, budget) suite.Require().NoError(err) // Claim the funds once msg := &types.MsgClaimBudget{ RecipientAddress: recipientAddr.String(), } suite.mockSendCoinsFromModuleToAccount(recipientAddr) _, err = suite.msgServer.ClaimBudget(suite.ctx, msg) suite.Require().NoError(err) }, recipientAddress: recipientAddr, expErr: true, expErrMsg: "budget period has not passed yet", }, "valid double claim attempt": { preRun: func() { oneMonthInSeconds := int64(30 * 24 * 60 * 60) // Approximate number of seconds in 1 month startTimeBeforeMonth := suite.ctx.BlockTime().Add(time.Duration(-oneMonthInSeconds) * time.Second) oneMonthPeriod := time.Duration(oneMonthInSeconds) * time.Second // Prepare the budget proposal with valid start time and period of 1 month (in seconds) budget := types.Budget{ RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTimeBeforeMonth, Tranches: 2, Period: &oneMonthPeriod, } err := suite.poolKeeper.BudgetProposal.Set(suite.ctx, recipientAddr, budget) suite.Require().NoError(err) // Claim the funds once msg := &types.MsgClaimBudget{ RecipientAddress: recipientAddr.String(), } suite.mockSendCoinsFromModuleToAccount(recipientAddr) _, err = suite.msgServer.ClaimBudget(suite.ctx, msg) suite.Require().NoError(err) // Create a new context with an updated block time to simulate a delay newBlockTime := suite.ctx.BlockTime().Add(time.Duration(oneMonthInSeconds) * time.Second) suite.ctx = suite.ctx.WithHeaderInfo(header.Info{ Time: newBlockTime, }) }, recipientAddress: recipientAddr, expErr: false, claimableFunds: sdk.NewInt64Coin("foo", 50), }, "budget ended for recipient": { preRun: func() { // Prepare the budget proposal with valid start time and period budget := types.Budget{ RecipientAddress: recipientAddr.String(), TotalBudget: &fooCoin, StartTime: &startTime, Tranches: 2, Period: &period, } err := suite.poolKeeper.BudgetProposal.Set(suite.ctx, recipientAddr, budget) suite.Require().NoError(err) // Claim the funds once msg := &types.MsgClaimBudget{ RecipientAddress: recipientAddr.String(), } suite.mockSendCoinsFromModuleToAccount(recipientAddr) _, err = suite.msgServer.ClaimBudget(suite.ctx, msg) suite.Require().NoError(err) // Create a new context with an updated block time to simulate a delay newBlockTime := suite.ctx.BlockTime().Add(60 * time.Second) suite.ctx = suite.ctx.WithHeaderInfo(header.Info{ Time: newBlockTime, }) // Claim the funds twice msg = &types.MsgClaimBudget{ RecipientAddress: recipientAddr.String(), } suite.mockSendCoinsFromModuleToAccount(recipientAddr) _, err = suite.msgServer.ClaimBudget(suite.ctx, msg) suite.Require().NoError(err) }, recipientAddress: recipientAddr, expErr: true, expErrMsg: "budget ended for recipient", }, } for name, tc := range testCases { suite.Run(name, func() { suite.SetupTest() if tc.preRun != nil { tc.preRun() } msg := &types.MsgClaimBudget{ RecipientAddress: tc.recipientAddress.String(), } suite.mockSendCoinsFromModuleToAccount(tc.recipientAddress) resp, err := suite.msgServer.ClaimBudget(suite.ctx, msg) if tc.expErr { suite.Require().Error(err) suite.Require().Contains(err.Error(), tc.expErrMsg) } else { suite.Require().NoError(err) suite.Require().Equal(tc.claimableFunds, resp.Amount) } }) } }