diff --git a/app/app.go b/app/app.go index b2f9c9a12..c151c3a77 100644 --- a/app/app.go +++ b/app/app.go @@ -204,6 +204,7 @@ var ( fundraisingtypes.ModuleName: nil, monitoringcmoduletypes.ModuleName: nil, monitoringpmoduletypes.ModuleName: nil, + claimmoduletypes.ModuleName: {authtypes.Minter, authtypes.Burner}, // this line is used by starport scaffolding # stargate/app/maccPerms } ) @@ -533,6 +534,7 @@ func New( keys[claimmoduletypes.StoreKey], keys[claimmoduletypes.MemStoreKey], app.GetSubspace(claimmoduletypes.ModuleName), + app.AuthKeeper, app.BankKeeper, ) diff --git a/config.yml b/config.yml index 1057a1cc8..d8e29d864 100644 --- a/config.yml +++ b/config.yml @@ -102,10 +102,10 @@ genesis: requiredAllocations: "10" benefits: maxBidAmount: "30000" - # 1/3 of the default unbonding period (21 days): one week + # represents 1/3 of the default unbonding period (21 days): one week registrationPeriod: "604800s" - # 2/3 of the default unbonding period (21 days): two weeks + # represents 2/3 of the default unbonding period (21 days): two weeks withdrawalDelay: "1209600s" client: typescript: - path: "ignite-ui/src/generated" \ No newline at end of file + path: "ignite-ui/src/generated" diff --git a/testutil/constructor/constructor.go b/testutil/constructor/constructor.go index 56eb1f4ad..0a342e197 100644 --- a/testutil/constructor/constructor.go +++ b/testutil/constructor/constructor.go @@ -35,6 +35,13 @@ func LastCommitInfo(votes ...Vote) abci.LastCommitInfo { return lci } +// Coin returns a sdk.Coin from a string +func Coin(t testing.TB, str string) sdk.Coin { + coin, err := sdk.ParseCoinNormalized(str) + require.NoError(t, err) + return coin +} + // Coins returns a sdk.Coins from a string func Coins(t testing.TB, str string) sdk.Coins { coins, err := sdk.ParseCoinsNormalized(str) diff --git a/testutil/keeper/claim.go b/testutil/keeper/claim.go deleted file mode 100644 index 313664103..000000000 --- a/testutil/keeper/claim.go +++ /dev/null @@ -1,54 +0,0 @@ -package keeper - -import ( - "testing" - - "github.com/cosmos/cosmos-sdk/codec" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" - "github.com/cosmos/cosmos-sdk/store" - storetypes "github.com/cosmos/cosmos-sdk/store/types" - sdk "github.com/cosmos/cosmos-sdk/types" - typesparams "github.com/cosmos/cosmos-sdk/x/params/types" - "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/libs/log" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" - tmdb "github.com/tendermint/tm-db" - - "github.com/tendermint/spn/x/claim/keeper" - "github.com/tendermint/spn/x/claim/types" -) - -func ClaimKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { - storeKey := sdk.NewKVStoreKey(types.StoreKey) - memStoreKey := storetypes.NewMemoryStoreKey(types.MemStoreKey) - - db := tmdb.NewMemDB() - stateStore := store.NewCommitMultiStore(db) - stateStore.MountStoreWithDB(storeKey, sdk.StoreTypeIAVL, db) - stateStore.MountStoreWithDB(memStoreKey, sdk.StoreTypeMemory, nil) - require.NoError(t, stateStore.LoadLatestVersion()) - - registry := codectypes.NewInterfaceRegistry() - cdc := codec.NewProtoCodec(registry) - - paramsSubspace := typesparams.NewSubspace(cdc, - types.Amino, - storeKey, - memStoreKey, - "ClaimParams", - ) - k := keeper.NewKeeper( - cdc, - storeKey, - memStoreKey, - paramsSubspace, - nil, - ) - - ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) - - // Initialize params - k.SetParams(ctx, types.DefaultParams()) - - return k, ctx -} diff --git a/testutil/keeper/initializer.go b/testutil/keeper/initializer.go index 9af332ea9..b64007e4a 100644 --- a/testutil/keeper/initializer.go +++ b/testutil/keeper/initializer.go @@ -54,6 +54,7 @@ var moduleAccountPerms = map[string][]string{ stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, rewardmoduletypes.ModuleName: {authtypes.Minter, authtypes.Burner}, fundraisingtypes.ModuleName: nil, + claimtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, } // initializer allows to initialize each module keeper @@ -413,6 +414,7 @@ func (i initializer) Participation( func (i initializer) Claim( paramKeeper paramskeeper.Keeper, + accountKeeper authkeeper.AccountKeeper, bankKeeper bankkeeper.Keeper, ) *claimkeeper.Keeper { storeKey := sdk.NewKVStoreKey(claimtypes.StoreKey) @@ -429,6 +431,7 @@ func (i initializer) Claim( storeKey, memStoreKey, subspace, + accountKeeper, bankKeeper, ) } diff --git a/testutil/keeper/keeper.go b/testutil/keeper/keeper.go index 704015fbb..560be0b93 100644 --- a/testutil/keeper/keeper.go +++ b/testutil/keeper/keeper.go @@ -6,6 +6,7 @@ import ( "time" sdk "github.com/cosmos/cosmos-sdk/types" + authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper" bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper" distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" @@ -54,6 +55,7 @@ type TestKeepers struct { RewardKeeper *rewardkeeper.Keeper MonitoringConsumerKeeper *monitoringckeeper.Keeper MonitoringProviderKeeper *monitoringpkeeper.Keeper + AccountKeeper authkeeper.AccountKeeper BankKeeper bankkeeper.Keeper IBCKeeper *ibckeeper.Keeper StakingKeeper stakingkeeper.Keeper @@ -101,7 +103,7 @@ func NewTestSetup(t testing.TB) (sdk.Context, TestKeepers, TestMsgServers) { []Channel{}, ) launchKeeper.SetMonitoringcKeeper(monitoringConsumerKeeper) - claimKeeper := initializer.Claim(paramKeeper, bankKeeper) + claimKeeper := initializer.Claim(paramKeeper, authKeeper, bankKeeper) require.NoError(t, initializer.StateStore.LoadLatestVersion()) // Create a context using a custom timestamp @@ -144,6 +146,7 @@ func NewTestSetup(t testing.TB) (sdk.Context, TestKeepers, TestMsgServers) { ProfileKeeper: profileKeeper, RewardKeeper: rewardKeeper, MonitoringConsumerKeeper: monitoringConsumerKeeper, + AccountKeeper: authKeeper, BankKeeper: bankKeeper, IBCKeeper: ibcKeeper, StakingKeeper: stakingKeeper, @@ -193,7 +196,7 @@ func NewTestSetupWithIBCMocks( channelMock, ) launchKeeper.SetMonitoringcKeeper(monitoringConsumerKeeper) - claimKeeper := initializer.Claim(paramKeeper, bankKeeper) + claimKeeper := initializer.Claim(paramKeeper, authKeeper, bankKeeper) require.NoError(t, initializer.StateStore.LoadLatestVersion()) // Create a context using a custom timestamp @@ -234,6 +237,7 @@ func NewTestSetupWithIBCMocks( ProfileKeeper: profileKeeper, RewardKeeper: rewardKeeper, MonitoringConsumerKeeper: monitoringConsumerKeeper, + AccountKeeper: authKeeper, BankKeeper: bankKeeper, IBCKeeper: ibcKeeper, StakingKeeper: stakingKeeper, diff --git a/x/claim/genesis.go b/x/claim/genesis.go index 60983732c..f3bb3a1ab 100644 --- a/x/claim/genesis.go +++ b/x/claim/genesis.go @@ -19,7 +19,9 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, genState types.GenesisState) k.SetMission(ctx, elem) } - k.SetAirdropSupply(ctx, genState.AirdropSupply) + if err := k.InitializeAirdropSupply(ctx, genState.AirdropSupply); err != nil { + panic("airdrop supply failed to initialize: " + err.Error()) + } // this line is used by starport scaffolding # genesis/module/init k.SetParams(ctx, genState.Params) diff --git a/x/claim/genesis_test.go b/x/claim/genesis_test.go index 8f1615ded..75c31226f 100644 --- a/x/claim/genesis_test.go +++ b/x/claim/genesis_test.go @@ -5,8 +5,7 @@ import ( "testing" "github.com/stretchr/testify/require" - - keepertest "github.com/tendermint/spn/testutil/keeper" + testkeeper "github.com/tendermint/spn/testutil/keeper" "github.com/tendermint/spn/testutil/nullify" "github.com/tendermint/spn/testutil/sample" "github.com/tendermint/spn/x/claim" @@ -45,9 +44,9 @@ func TestGenesis(t *testing.T) { // this line is used by starport scaffolding # genesis/test/state } - k, ctx := keepertest.ClaimKeeper(t) - claim.InitGenesis(ctx, *k, genesisState) - got := claim.ExportGenesis(ctx, *k) + ctx, tk, _ := testkeeper.NewTestSetup(t) + claim.InitGenesis(ctx, *tk.ClaimKeeper, genesisState) + got := claim.ExportGenesis(ctx, *tk.ClaimKeeper) require.NotNil(t, got) nullify.Fill(&genesisState) diff --git a/x/claim/keeper/airdrop_supply.go b/x/claim/keeper/airdrop_supply.go index 208f57e38..7c3f39d3b 100644 --- a/x/claim/keeper/airdrop_supply.go +++ b/x/claim/keeper/airdrop_supply.go @@ -4,6 +4,7 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" + spnerrors "github.com/tendermint/spn/pkg/errors" "github.com/tendermint/spn/x/claim/types" ) @@ -26,3 +27,34 @@ func (k Keeper) GetAirdropSupply(ctx sdk.Context) (val sdk.Coin, found bool) { k.cdc.MustUnmarshal(b, &val) return val, true } + +// RemoveAirdropSupply removes the AirdropSupply from the store +func (k Keeper) RemoveAirdropSupply(ctx sdk.Context) { + store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.AirdropSupplyKey)) + store.Delete([]byte{0}) +} + +// InitializeAirdropSupply set the airdrop supply in the store and set the module balance +func (k Keeper) InitializeAirdropSupply(ctx sdk.Context, airdropSupply sdk.Coin) error { + // get the eventual existing balance of the module for the airdrop supply + moduleBalance := k.bankKeeper.GetBalance( + ctx, + k.accountKeeper.GetModuleAddress(types.ModuleName), + airdropSupply.Denom, + ) + + // if the module has an existing balance, we burn the entire balance + if moduleBalance.IsPositive() { + if err := k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(moduleBalance)); err != nil { + return spnerrors.Criticalf("can't burn module balance %s", err.Error()) + } + } + + // set the module balance with the airdrop supply + if err := k.bankKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(airdropSupply)); err != nil { + return spnerrors.Criticalf("can't mint airdrop suply into module balance %s", err.Error()) + } + + k.SetAirdropSupply(ctx, airdropSupply) + return nil +} diff --git a/x/claim/keeper/airdrop_supply_test.go b/x/claim/keeper/airdrop_supply_test.go index aa74ff8a6..b3b3972d8 100644 --- a/x/claim/keeper/airdrop_supply_test.go +++ b/x/claim/keeper/airdrop_supply_test.go @@ -6,25 +6,79 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "github.com/tendermint/spn/testutil/keeper" + tc "github.com/tendermint/spn/testutil/constructor" + testkeeper "github.com/tendermint/spn/testutil/keeper" "github.com/tendermint/spn/testutil/nullify" "github.com/tendermint/spn/testutil/sample" - "github.com/tendermint/spn/x/claim/keeper" + claim "github.com/tendermint/spn/x/claim/types" ) -func createTestAirdropSupply(keeper *keeper.Keeper, ctx sdk.Context) sdk.Coin { - item := sample.Coin(r) - keeper.SetAirdropSupply(ctx, item) - return item -} - func TestAirdropSupplyGet(t *testing.T) { - k, ctx := keepertest.ClaimKeeper(t) - item := createTestAirdropSupply(k, ctx) - rst, found := k.GetAirdropSupply(ctx) + ctx, tk, _ := testkeeper.NewTestSetup(t) + + sampleSupply := sample.Coin(r) + tk.ClaimKeeper.SetAirdropSupply(ctx, sampleSupply) + + rst, found := tk.ClaimKeeper.GetAirdropSupply(ctx) require.True(t, found) require.Equal(t, - nullify.Fill(&item), + nullify.Fill(&sampleSupply), nullify.Fill(&rst), ) } + +func TestAirdropSupplyRemove(t *testing.T) { + ctx, tk, _ := testkeeper.NewTestSetup(t) + + tk.ClaimKeeper.SetAirdropSupply(ctx, sample.Coin(r)) + _, found := tk.ClaimKeeper.GetAirdropSupply(ctx) + require.True(t, found) + tk.ClaimKeeper.RemoveAirdropSupply(ctx) + _, found = tk.ClaimKeeper.GetAirdropSupply(ctx) + require.False(t, found) +} + +func TestKeeper_InitializeAirdropSupply(t *testing.T) { + // TODO: use mock for bank module to test critical errors + // https://github.com/tendermint/spn/issues/838 + ctx, tk, _ := testkeeper.NewTestSetup(t) + + tests := []struct { + name string + airdropSupply sdk.Coin + }{ + { + name: "should allows setting airdrop supply", + airdropSupply: tc.Coin(t, "10000foo"), + }, + { + name: "should allows specifying a new token for the supply", + airdropSupply: tc.Coin(t, "125000bar"), + }, + { + name: "should allows modifying a token for the supply", + airdropSupply: tc.Coin(t, "525000bar"), + }, + { + name: "should allows setting airdrop supply to zero", + airdropSupply: sdk.NewCoin("foo", sdk.ZeroInt()), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tk.ClaimKeeper.InitializeAirdropSupply(ctx, tt.airdropSupply) + require.NoError(t, err) + + airdropSupply, found := tk.ClaimKeeper.GetAirdropSupply(ctx) + require.True(t, found) + require.True(t, airdropSupply.IsEqual(tt.airdropSupply)) + + moduleBalance := tk.BankKeeper.GetBalance( + ctx, + tk.AccountKeeper.GetModuleAddress(claim.ModuleName), + airdropSupply.Denom, + ) + require.True(t, moduleBalance.IsEqual(tt.airdropSupply)) + }) + } +} diff --git a/x/claim/keeper/claim_record.go b/x/claim/keeper/claim_record.go index fe6ed7aa0..dc98341ad 100644 --- a/x/claim/keeper/claim_record.go +++ b/x/claim/keeper/claim_record.go @@ -17,15 +17,10 @@ func (k Keeper) SetClaimRecord(ctx sdk.Context, claimRecord types.ClaimRecord) { } // GetClaimRecord returns a claimRecord from its index -func (k Keeper) GetClaimRecord( - ctx sdk.Context, - index string, -) (val types.ClaimRecord, found bool) { +func (k Keeper) GetClaimRecord(ctx sdk.Context, address string) (val types.ClaimRecord, found bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimRecordKeyPrefix)) - b := store.Get(types.ClaimRecordKey( - index, - )) + b := store.Get(types.ClaimRecordKey(address)) if b == nil { return val, false } @@ -37,12 +32,10 @@ func (k Keeper) GetClaimRecord( // RemoveClaimRecord removes a claimRecord from the store func (k Keeper) RemoveClaimRecord( ctx sdk.Context, - index string, + address string, ) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ClaimRecordKeyPrefix)) - store.Delete(types.ClaimRecordKey( - index, - )) + store.Delete(types.ClaimRecordKey(address)) } // GetAllClaimRecord returns all claimRecord diff --git a/x/claim/keeper/claim_record_test.go b/x/claim/keeper/claim_record_test.go index b099bb9e0..174f43a96 100644 --- a/x/claim/keeper/claim_record_test.go +++ b/x/claim/keeper/claim_record_test.go @@ -6,7 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "github.com/tendermint/spn/testutil/keeper" + testkeeper "github.com/tendermint/spn/testutil/keeper" "github.com/tendermint/spn/testutil/nullify" "github.com/tendermint/spn/testutil/sample" "github.com/tendermint/spn/x/claim/keeper" @@ -25,10 +25,11 @@ func createNClaimRecord(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.C } func TestClaimRecordGet(t *testing.T) { - k, ctx := keepertest.ClaimKeeper(t) - items := createNClaimRecord(k, ctx, 10) + ctx, tk, _ := testkeeper.NewTestSetup(t) + + items := createNClaimRecord(tk.ClaimKeeper, ctx, 10) for _, item := range items { - rst, found := k.GetClaimRecord(ctx, + rst, found := tk.ClaimKeeper.GetClaimRecord(ctx, item.Address, ) require.True(t, found) @@ -40,13 +41,14 @@ func TestClaimRecordGet(t *testing.T) { } func TestClaimRecordRemove(t *testing.T) { - k, ctx := keepertest.ClaimKeeper(t) - items := createNClaimRecord(k, ctx, 10) + ctx, tk, _ := testkeeper.NewTestSetup(t) + + items := createNClaimRecord(tk.ClaimKeeper, ctx, 10) for _, item := range items { - k.RemoveClaimRecord(ctx, + tk.ClaimKeeper.RemoveClaimRecord(ctx, item.Address, ) - _, found := k.GetClaimRecord(ctx, + _, found := tk.ClaimKeeper.GetClaimRecord(ctx, item.Address, ) require.False(t, found) @@ -54,10 +56,11 @@ func TestClaimRecordRemove(t *testing.T) { } func TestClaimRecordGetAll(t *testing.T) { - k, ctx := keepertest.ClaimKeeper(t) - items := createNClaimRecord(k, ctx, 10) + ctx, tk, _ := testkeeper.NewTestSetup(t) + + items := createNClaimRecord(tk.ClaimKeeper, ctx, 10) require.ElementsMatch(t, nullify.Fill(items), - nullify.Fill(k.GetAllClaimRecord(ctx)), + nullify.Fill(tk.ClaimKeeper.GetAllClaimRecord(ctx)), ) } diff --git a/x/claim/keeper/grpc_params_test.go b/x/claim/keeper/grpc_params_test.go index d4ee89e61..e9ad30a56 100644 --- a/x/claim/keeper/grpc_params_test.go +++ b/x/claim/keeper/grpc_params_test.go @@ -11,12 +11,13 @@ import ( ) func TestParamsQuery(t *testing.T) { - keeper, ctx := testkeeper.ClaimKeeper(t) + ctx, tk, _ := testkeeper.NewTestSetup(t) + wctx := sdk.WrapSDKContext(ctx) params := types.DefaultParams() - keeper.SetParams(ctx, params) + tk.ClaimKeeper.SetParams(ctx, params) - response, err := keeper.Params(wctx, &types.QueryParamsRequest{}) + response, err := tk.ClaimKeeper.Params(wctx, &types.QueryParamsRequest{}) require.NoError(t, err) require.Equal(t, &types.QueryParamsResponse{Params: params}, response) } diff --git a/x/claim/keeper/grpc_query_airdrop_supply_test.go b/x/claim/keeper/grpc_query_airdrop_supply_test.go index 98bb4ead0..6e4be7a38 100644 --- a/x/claim/keeper/grpc_query_airdrop_supply_test.go +++ b/x/claim/keeper/grpc_query_airdrop_supply_test.go @@ -8,15 +8,20 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - keepertest "github.com/tendermint/spn/testutil/keeper" + testkeeper "github.com/tendermint/spn/testutil/keeper" "github.com/tendermint/spn/testutil/nullify" + "github.com/tendermint/spn/testutil/sample" "github.com/tendermint/spn/x/claim/types" ) func TestAirdropSupplyQuery(t *testing.T) { - keeper, ctx := keepertest.ClaimKeeper(t) - wctx := sdk.WrapSDKContext(ctx) - item := createTestAirdropSupply(keeper, ctx) + var ( + ctx, tk, _ = testkeeper.NewTestSetup(t) + wctx = sdk.WrapSDKContext(ctx) + sampleSupply = sample.Coin(r) + ) + tk.ClaimKeeper.SetAirdropSupply(ctx, sampleSupply) + for _, tc := range []struct { desc string request *types.QueryGetAirdropSupplyRequest @@ -26,7 +31,7 @@ func TestAirdropSupplyQuery(t *testing.T) { { desc: "First", request: &types.QueryGetAirdropSupplyRequest{}, - response: &types.QueryGetAirdropSupplyResponse{AirdropSupply: item}, + response: &types.QueryGetAirdropSupplyResponse{AirdropSupply: sampleSupply}, }, { desc: "InvalidRequest", @@ -34,7 +39,7 @@ func TestAirdropSupplyQuery(t *testing.T) { }, } { t.Run(tc.desc, func(t *testing.T) { - response, err := keeper.AirdropSupply(wctx, tc.request) + response, err := tk.ClaimKeeper.AirdropSupply(wctx, tc.request) if tc.err != nil { require.ErrorIs(t, err, tc.err) } else { diff --git a/x/claim/keeper/grpc_query_claim_record_test.go b/x/claim/keeper/grpc_query_claim_record_test.go index e606599e6..fcfa50d69 100644 --- a/x/claim/keeper/grpc_query_claim_record_test.go +++ b/x/claim/keeper/grpc_query_claim_record_test.go @@ -10,15 +10,18 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - keepertest "github.com/tendermint/spn/testutil/keeper" + testkeeper "github.com/tendermint/spn/testutil/keeper" "github.com/tendermint/spn/testutil/nullify" "github.com/tendermint/spn/x/claim/types" ) func TestClaimRecordQuerySingle(t *testing.T) { - keeper, ctx := keepertest.ClaimKeeper(t) - wctx := sdk.WrapSDKContext(ctx) - msgs := createNClaimRecord(keeper, ctx, 2) + var ( + ctx, tk, _ = testkeeper.NewTestSetup(t) + wctx = sdk.WrapSDKContext(ctx) + msgs = createNClaimRecord(tk.ClaimKeeper, ctx, 2) + ) + for _, tc := range []struct { desc string request *types.QueryGetClaimRecordRequest @@ -52,7 +55,7 @@ func TestClaimRecordQuerySingle(t *testing.T) { }, } { t.Run(tc.desc, func(t *testing.T) { - response, err := keeper.ClaimRecord(wctx, tc.request) + response, err := tk.ClaimKeeper.ClaimRecord(wctx, tc.request) if tc.err != nil { require.ErrorIs(t, err, tc.err) } else { @@ -67,9 +70,11 @@ func TestClaimRecordQuerySingle(t *testing.T) { } func TestClaimRecordQueryPaginated(t *testing.T) { - keeper, ctx := keepertest.ClaimKeeper(t) - wctx := sdk.WrapSDKContext(ctx) - msgs := createNClaimRecord(keeper, ctx, 5) + var ( + ctx, tk, _ = testkeeper.NewTestSetup(t) + wctx = sdk.WrapSDKContext(ctx) + msgs = createNClaimRecord(tk.ClaimKeeper, ctx, 5) + ) request := func(next []byte, offset, limit uint64, total bool) *types.QueryAllClaimRecordRequest { return &types.QueryAllClaimRecordRequest{ @@ -84,7 +89,7 @@ func TestClaimRecordQueryPaginated(t *testing.T) { t.Run("ByOffset", func(t *testing.T) { step := 2 for i := 0; i < len(msgs); i += step { - resp, err := keeper.ClaimRecordAll(wctx, request(nil, uint64(i), uint64(step), false)) + resp, err := tk.ClaimKeeper.ClaimRecordAll(wctx, request(nil, uint64(i), uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.ClaimRecord), step) require.Subset(t, @@ -97,7 +102,7 @@ func TestClaimRecordQueryPaginated(t *testing.T) { step := 2 var next []byte for i := 0; i < len(msgs); i += step { - resp, err := keeper.ClaimRecordAll(wctx, request(next, 0, uint64(step), false)) + resp, err := tk.ClaimKeeper.ClaimRecordAll(wctx, request(next, 0, uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.ClaimRecord), step) require.Subset(t, @@ -108,7 +113,7 @@ func TestClaimRecordQueryPaginated(t *testing.T) { } }) t.Run("Total", func(t *testing.T) { - resp, err := keeper.ClaimRecordAll(wctx, request(nil, 0, 0, true)) + resp, err := tk.ClaimKeeper.ClaimRecordAll(wctx, request(nil, 0, 0, true)) require.NoError(t, err) require.Equal(t, len(msgs), int(resp.Pagination.Total)) require.ElementsMatch(t, @@ -117,7 +122,7 @@ func TestClaimRecordQueryPaginated(t *testing.T) { ) }) t.Run("InvalidRequest", func(t *testing.T) { - _, err := keeper.ClaimRecordAll(wctx, nil) + _, err := tk.ClaimKeeper.ClaimRecordAll(wctx, nil) require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "invalid request")) }) } diff --git a/x/claim/keeper/grpc_query_mission_test.go b/x/claim/keeper/grpc_query_mission_test.go index d5c1d6a41..54f5d73e7 100644 --- a/x/claim/keeper/grpc_query_mission_test.go +++ b/x/claim/keeper/grpc_query_mission_test.go @@ -10,15 +10,16 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - keepertest "github.com/tendermint/spn/testutil/keeper" + testkeeper "github.com/tendermint/spn/testutil/keeper" "github.com/tendermint/spn/testutil/nullify" "github.com/tendermint/spn/x/claim/types" ) func TestMissionQuerySingle(t *testing.T) { - keeper, ctx := keepertest.ClaimKeeper(t) + ctx, tk, _ := testkeeper.NewTestSetup(t) + wctx := sdk.WrapSDKContext(ctx) - msgs := createNMission(keeper, ctx, 2) + msgs := createNMission(tk.ClaimKeeper, ctx, 2) for _, tc := range []struct { desc string request *types.QueryGetMissionRequest @@ -46,7 +47,7 @@ func TestMissionQuerySingle(t *testing.T) { }, } { t.Run(tc.desc, func(t *testing.T) { - response, err := keeper.Mission(wctx, tc.request) + response, err := tk.ClaimKeeper.Mission(wctx, tc.request) if tc.err != nil { require.ErrorIs(t, err, tc.err) } else { @@ -61,9 +62,10 @@ func TestMissionQuerySingle(t *testing.T) { } func TestMissionQueryPaginated(t *testing.T) { - keeper, ctx := keepertest.ClaimKeeper(t) + ctx, tk, _ := testkeeper.NewTestSetup(t) + wctx := sdk.WrapSDKContext(ctx) - msgs := createNMission(keeper, ctx, 5) + msgs := createNMission(tk.ClaimKeeper, ctx, 5) request := func(next []byte, offset, limit uint64, total bool) *types.QueryAllMissionRequest { return &types.QueryAllMissionRequest{ @@ -78,7 +80,7 @@ func TestMissionQueryPaginated(t *testing.T) { t.Run("ByOffset", func(t *testing.T) { step := 2 for i := 0; i < len(msgs); i += step { - resp, err := keeper.MissionAll(wctx, request(nil, uint64(i), uint64(step), false)) + resp, err := tk.ClaimKeeper.MissionAll(wctx, request(nil, uint64(i), uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.Mission), step) require.Subset(t, @@ -91,7 +93,7 @@ func TestMissionQueryPaginated(t *testing.T) { step := 2 var next []byte for i := 0; i < len(msgs); i += step { - resp, err := keeper.MissionAll(wctx, request(next, 0, uint64(step), false)) + resp, err := tk.ClaimKeeper.MissionAll(wctx, request(next, 0, uint64(step), false)) require.NoError(t, err) require.LessOrEqual(t, len(resp.Mission), step) require.Subset(t, @@ -102,7 +104,7 @@ func TestMissionQueryPaginated(t *testing.T) { } }) t.Run("Total", func(t *testing.T) { - resp, err := keeper.MissionAll(wctx, request(nil, 0, 0, true)) + resp, err := tk.ClaimKeeper.MissionAll(wctx, request(nil, 0, 0, true)) require.NoError(t, err) require.Equal(t, len(msgs), int(resp.Pagination.Total)) require.ElementsMatch(t, @@ -111,7 +113,7 @@ func TestMissionQueryPaginated(t *testing.T) { ) }) t.Run("InvalidRequest", func(t *testing.T) { - _, err := keeper.MissionAll(wctx, nil) + _, err := tk.ClaimKeeper.MissionAll(wctx, nil) require.ErrorIs(t, err, status.Error(codes.InvalidArgument, "invalid request")) }) } diff --git a/x/claim/keeper/keeper.go b/x/claim/keeper/keeper.go index 383e33869..dc31b6806 100644 --- a/x/claim/keeper/keeper.go +++ b/x/claim/keeper/keeper.go @@ -14,12 +14,12 @@ import ( type ( Keeper struct { - cdc codec.BinaryCodec - storeKey sdk.StoreKey - memKey sdk.StoreKey - paramstore paramtypes.Subspace - - bankKeeper types.BankKeeper + cdc codec.BinaryCodec + storeKey sdk.StoreKey + memKey sdk.StoreKey + paramstore paramtypes.Subspace + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper } ) @@ -28,7 +28,7 @@ func NewKeeper( storeKey, memKey sdk.StoreKey, ps paramtypes.Subspace, - + accountKeeper types.AccountKeeper, bankKeeper types.BankKeeper, ) *Keeper { // set KeyTable if it has not already been set @@ -37,11 +37,12 @@ func NewKeeper( } return &Keeper{ - cdc: cdc, - storeKey: storeKey, - memKey: memKey, - paramstore: ps, - bankKeeper: bankKeeper, + cdc: cdc, + storeKey: storeKey, + memKey: memKey, + paramstore: ps, + accountKeeper: accountKeeper, + bankKeeper: bankKeeper, } } diff --git a/x/claim/keeper/mission.go b/x/claim/keeper/mission.go index 02d631c44..9f2943fb9 100644 --- a/x/claim/keeper/mission.go +++ b/x/claim/keeper/mission.go @@ -5,10 +5,19 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + spnerrors "github.com/tendermint/spn/pkg/errors" "github.com/tendermint/spn/x/claim/types" ) +// GetMissionIDBytes returns the byte representation of the ID +func GetMissionIDBytes(id uint64) []byte { + bz := make([]byte, 8) + binary.BigEndian.PutUint64(bz, id) + return bz +} + // SetMission set a specific mission in the store func (k Keeper) SetMission(ctx sdk.Context, mission types.Mission) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.MissionKey)) @@ -49,14 +58,59 @@ func (k Keeper) GetAllMission(ctx sdk.Context) (list []types.Mission) { return } -// GetMissionIDBytes returns the byte representation of the ID -func GetMissionIDBytes(id uint64) []byte { - bz := make([]byte, 8) - binary.BigEndian.PutUint64(bz, id) - return bz -} +// CompleteMission triggers the completion of the mission and distribute the claimable portion of airdrop to the user +// the method fails if the mission has already been completed +func (k Keeper) CompleteMission(ctx sdk.Context, missionID uint64, address string) error { + airdropSupply, found := k.GetAirdropSupply(ctx) + if !found { + return sdkerrors.Wrapf(types.ErrAirdropSupplyNotFound, "airdrop supply is not defined") + } + + // retrieve mission + mission, found := k.GetMission(ctx, missionID) + if !found { + return sdkerrors.Wrapf(types.ErrMissionNotFound, "mission %d not found", missionID) + } + + // retrieve claim record of the user + claimRecord, found := k.GetClaimRecord(ctx, address) + if !found { + return sdkerrors.Wrapf(types.ErrClaimRecordNotFound, "claim record not found for address %s", address) + } + + // check if the mission is already complted for the claim record + if claimRecord.IsMissionCompleted(missionID) { + return sdkerrors.Wrapf( + types.ErrMissionCompleted, + "mission %d completed for address %s", + missionID, + address, + ) + } + claimRecord.CompletedMissions = append(claimRecord.CompletedMissions, missionID) + + // calculate claimable from mission weight and claim + claimableAmount := mission.Weight.Mul(claimRecord.Claimable.ToDec()).TruncateInt() + claimable := sdk.NewCoins(sdk.NewCoin(airdropSupply.Denom, claimableAmount)) + + // decrease airdrop supply + airdropSupply.Amount = airdropSupply.Amount.Sub(claimableAmount) + if airdropSupply.Amount.IsNegative() { + return spnerrors.Critical("airdrop supply is lower than total claimable") + } + + // send claimable to the user + claimer, err := sdk.AccAddressFromBech32(address) + if err != nil { + return spnerrors.Criticalf("invalid claimer address %s", err.Error()) + } + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, claimer, claimable); err != nil { + return spnerrors.Criticalf("can't send claimable coins %s", err.Error()) + } + + // update store + k.SetAirdropSupply(ctx, airdropSupply) + k.SetClaimRecord(ctx, claimRecord) -// GetMissionIDFromBytes returns ID in uint64 format from a byte array -func GetMissionIDFromBytes(bz []byte) uint64 { - return binary.BigEndian.Uint64(bz) + return nil } diff --git a/x/claim/keeper/mission_test.go b/x/claim/keeper/mission_test.go index a6c0c9228..0d5f415fc 100644 --- a/x/claim/keeper/mission_test.go +++ b/x/claim/keeper/mission_test.go @@ -6,8 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "github.com/tendermint/spn/testutil/keeper" + spnerrors "github.com/tendermint/spn/pkg/errors" + tc "github.com/tendermint/spn/testutil/constructor" + testkeeper "github.com/tendermint/spn/testutil/keeper" "github.com/tendermint/spn/testutil/nullify" + "github.com/tendermint/spn/testutil/sample" "github.com/tendermint/spn/x/claim/keeper" "github.com/tendermint/spn/x/claim/types" ) @@ -23,10 +26,11 @@ func createNMission(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.Missi } func TestMissionGet(t *testing.T) { - k, ctx := keepertest.ClaimKeeper(t) - items := createNMission(k, ctx, 10) + ctx, tk, _ := testkeeper.NewTestSetup(t) + + items := createNMission(tk.ClaimKeeper, ctx, 10) for _, item := range items { - got, found := k.GetMission(ctx, item.MissionID) + got, found := tk.ClaimKeeper.GetMission(ctx, item.MissionID) require.True(t, found) require.Equal(t, nullify.Fill(&item), @@ -35,21 +39,306 @@ func TestMissionGet(t *testing.T) { } } +func TestMissionGetAll(t *testing.T) { + ctx, tk, _ := testkeeper.NewTestSetup(t) + + items := createNMission(tk.ClaimKeeper, ctx, 10) + require.ElementsMatch(t, + nullify.Fill(items), + nullify.Fill(tk.ClaimKeeper.GetAllMission(ctx)), + ) +} + func TestMissionRemove(t *testing.T) { - k, ctx := keepertest.ClaimKeeper(t) - items := createNMission(k, ctx, 10) + ctx, tk, _ := testkeeper.NewTestSetup(t) + + items := createNMission(tk.ClaimKeeper, ctx, 10) for _, item := range items { - k.RemoveMission(ctx, item.MissionID) - _, found := k.GetMission(ctx, item.MissionID) + tk.ClaimKeeper.RemoveMission(ctx, item.MissionID) + _, found := tk.ClaimKeeper.GetMission(ctx, item.MissionID) require.False(t, found) } } -func TestMissionGetAll(t *testing.T) { - k, ctx := keepertest.ClaimKeeper(t) - items := createNMission(k, ctx, 10) - require.ElementsMatch(t, - nullify.Fill(items), - nullify.Fill(k.GetAllMission(ctx)), - ) +func TestKeeper_CompleteMission(t *testing.T) { + ctx, tk, _ := testkeeper.NewTestSetup(t) + + type inputState struct { + noAirdropSupply bool + noMission bool + noClaimRecord bool + airdropSupply sdk.Coin + mission types.Mission + claimRecord types.ClaimRecord + } + + // prepare addresses + var addr []string + for i := 0; i < 20; i++ { + addr = append(addr, sample.Address(r)) + } + + tests := []struct { + name string + inputState inputState + missionID uint64 + address string + expectedBalance sdk.Coin + err error + }{ + { + name: "should fail if no airdrop supply", + inputState: inputState{ + noAirdropSupply: true, + claimRecord: sample.ClaimRecord(r), + mission: sample.Mission(r), + }, + missionID: 1, + address: sample.Address(r), + err: types.ErrAirdropSupplyNotFound, + }, + { + name: "should fail if no mission", + inputState: inputState{ + noMission: true, + airdropSupply: sample.Coin(r), + claimRecord: types.ClaimRecord{ + Address: addr[0], + Claimable: sdk.OneInt(), + CompletedMissions: []uint64{1}, + }, + }, + missionID: 1, + address: addr[0], + err: types.ErrMissionNotFound, + }, + { + name: "should fail if no claim record", + inputState: inputState{ + noClaimRecord: true, + airdropSupply: sample.Coin(r), + mission: types.Mission{ + MissionID: 1, + Weight: sdk.OneDec(), + }, + }, + missionID: 1, + address: sample.Address(r), + err: types.ErrClaimRecordNotFound, + }, + { + name: "should fail if mission already completed", + inputState: inputState{ + airdropSupply: sample.Coin(r), + mission: types.Mission{ + MissionID: 1, + Weight: sdk.OneDec(), + }, + claimRecord: types.ClaimRecord{ + Address: addr[1], + Claimable: sdk.OneInt(), + CompletedMissions: []uint64{1}, + }, + }, + missionID: 1, + address: addr[1], + err: types.ErrMissionCompleted, + }, + { + name: "should fail with critical if claimable amount is greater than module supply", + inputState: inputState{ + airdropSupply: tc.Coin(t, "1000foo"), + mission: types.Mission{ + MissionID: 1, + Weight: sdk.OneDec(), + }, + claimRecord: types.ClaimRecord{ + Address: addr[2], + Claimable: sdk.NewIntFromUint64(10000), + }, + }, + missionID: 1, + address: addr[2], + err: spnerrors.ErrCritical, + }, + { + name: "should fail with critical if claimer address is not bech32", + inputState: inputState{ + airdropSupply: tc.Coin(t, "1000foo"), + mission: types.Mission{ + MissionID: 1, + Weight: sdk.OneDec(), + }, + claimRecord: types.ClaimRecord{ + Address: "invalid", + Claimable: sdk.OneInt(), + }, + }, + missionID: 1, + address: "invalid", + err: spnerrors.ErrCritical, + }, + { + name: "should allow distributing full airdrop to one account, one mission", + inputState: inputState{ + airdropSupply: tc.Coin(t, "1000foo"), + mission: types.Mission{ + MissionID: 1, + Weight: sdk.OneDec(), + }, + claimRecord: types.ClaimRecord{ + Address: addr[3], + Claimable: sdk.NewIntFromUint64(1000), + }, + }, + missionID: 1, + address: addr[3], + expectedBalance: tc.Coin(t, "1000foo"), + }, + { + name: "should allow distributing no fund for mission with 0 weight", + inputState: inputState{ + airdropSupply: tc.Coin(t, "1000foo"), + mission: types.Mission{ + MissionID: 1, + Weight: sdk.ZeroDec(), + }, + claimRecord: types.ClaimRecord{ + Address: addr[4], + Claimable: sdk.NewIntFromUint64(1000), + }, + }, + missionID: 1, + address: addr[4], + expectedBalance: tc.Coin(t, "0foo"), + }, + { + name: "should allow distributing half for mission with 0.5 weight", + inputState: inputState{ + airdropSupply: tc.Coin(t, "1000foo"), + mission: types.Mission{ + MissionID: 1, + Weight: tc.Dec(t, "0.5"), + }, + claimRecord: types.ClaimRecord{ + Address: addr[5], + Claimable: sdk.NewIntFromUint64(500), + }, + }, + missionID: 1, + address: addr[5], + expectedBalance: tc.Coin(t, "250foo"), + }, + { + name: "should allow distributing half for mission with 0.5 weight and truncate decimal", + inputState: inputState{ + airdropSupply: tc.Coin(t, "1000foo"), + mission: types.Mission{ + MissionID: 1, + Weight: tc.Dec(t, "0.5"), + }, + claimRecord: types.ClaimRecord{ + Address: addr[6], + Claimable: sdk.NewIntFromUint64(201), + }, + }, + missionID: 1, + address: addr[6], + expectedBalance: tc.Coin(t, "100foo"), + }, + { + name: "should allow distributing no fund for empty claim record", + inputState: inputState{ + airdropSupply: tc.Coin(t, "1000foo"), + mission: types.Mission{ + MissionID: 1, + Weight: tc.Dec(t, "0.5"), + }, + claimRecord: types.ClaimRecord{ + Address: addr[7], + Claimable: sdk.ZeroInt(), + }, + }, + missionID: 1, + address: addr[7], + expectedBalance: tc.Coin(t, "0foo"), + }, + { + name: "should allow distributing airdrop with other already completed missions", + inputState: inputState{ + airdropSupply: tc.Coin(t, "10000bar"), + mission: types.Mission{ + MissionID: 3, + Weight: tc.Dec(t, "0.3"), + }, + claimRecord: types.ClaimRecord{ + Address: addr[8], + Claimable: sdk.NewIntFromUint64(10000), + CompletedMissions: []uint64{0, 1, 2, 4, 5, 6}, + }, + }, + missionID: 3, + address: addr[8], + expectedBalance: tc.Coin(t, "3000bar"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // initialize input state + if !tt.inputState.noAirdropSupply { + err := tk.ClaimKeeper.InitializeAirdropSupply(ctx, tt.inputState.airdropSupply) + require.NoError(t, err) + } + if !tt.inputState.noMission { + tk.ClaimKeeper.SetMission(ctx, tt.inputState.mission) + } + if !tt.inputState.noClaimRecord { + tk.ClaimKeeper.SetClaimRecord(ctx, tt.inputState.claimRecord) + } + + err := tk.ClaimKeeper.CompleteMission(ctx, tt.missionID, tt.address) + if tt.err != nil { + require.ErrorIs(t, err, tt.err) + } else { + require.NoError(t, err) + + // funds are distributed to the user + sdkAddr, err := sdk.AccAddressFromBech32(tt.address) + require.NoError(t, err) + balance := tk.BankKeeper.GetBalance(ctx, sdkAddr, tt.inputState.airdropSupply.Denom) + require.True(t, balance.IsEqual(tt.expectedBalance), + "expected balance after mission complete: %s, actual balance: %s", + tt.expectedBalance.String(), + balance.String(), + ) + + // completed mission is added in claim record + claimRecord, found := tk.ClaimKeeper.GetClaimRecord(ctx, tt.address) + require.True(t, found) + require.True(t, claimRecord.IsMissionCompleted(tt.missionID)) + + // airdrop supply is updated with distributed balance + airdropSupply, found := tk.ClaimKeeper.GetAirdropSupply(ctx) + require.True(t, found) + expectedAidropSupply := tt.inputState.airdropSupply.Sub(tt.expectedBalance) + + require.True(t, airdropSupply.IsEqual(expectedAidropSupply), + "expected airdrop supply after mission complete: %s, actual supply: %s", + expectedAidropSupply, + airdropSupply, + ) + } + + // clear input state + if !tt.inputState.noAirdropSupply { + tk.ClaimKeeper.RemoveAirdropSupply(ctx) + } + if !tt.inputState.noMission { + tk.ClaimKeeper.RemoveMission(ctx, tt.inputState.mission.MissionID) + } + if !tt.inputState.noClaimRecord { + tk.ClaimKeeper.RemoveClaimRecord(ctx, tt.inputState.claimRecord.Address) + } + }) + } } diff --git a/x/claim/keeper/msg_test.go b/x/claim/keeper/msg_test.go index 2133823f1..ba7b53eda 100644 --- a/x/claim/keeper/msg_test.go +++ b/x/claim/keeper/msg_test.go @@ -6,12 +6,13 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - keepertest "github.com/tendermint/spn/testutil/keeper" + testkeeper "github.com/tendermint/spn/testutil/keeper" "github.com/tendermint/spn/x/claim/keeper" "github.com/tendermint/spn/x/claim/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { - k, ctx := keepertest.ClaimKeeper(t) - return keeper.NewMsgServerImpl(*k), sdk.WrapSDKContext(ctx) + ctx, tk, _ := testkeeper.NewTestSetup(t) + + return keeper.NewMsgServerImpl(*tk.ClaimKeeper), sdk.WrapSDKContext(ctx) } diff --git a/x/claim/keeper/params_test.go b/x/claim/keeper/params_test.go index 3e5f8d903..1e12e51d0 100644 --- a/x/claim/keeper/params_test.go +++ b/x/claim/keeper/params_test.go @@ -10,10 +10,11 @@ import ( ) func TestGetParams(t *testing.T) { - k, ctx := testkeeper.ClaimKeeper(t) + ctx, tk, _ := testkeeper.NewTestSetup(t) + params := types.DefaultParams() - k.SetParams(ctx, params) + tk.ClaimKeeper.SetParams(ctx, params) - require.EqualValues(t, params, k.GetParams(ctx)) + require.EqualValues(t, params, tk.ClaimKeeper.GetParams(ctx)) } diff --git a/x/claim/module_simulation.go b/x/claim/module_simulation.go index 17e5dd4a6..543ea7016 100644 --- a/x/claim/module_simulation.go +++ b/x/claim/module_simulation.go @@ -35,7 +35,8 @@ func (AppModule) GenerateGenesisState(simState *module.SimulationState) { accs[i] = acc.Address.String() } claimGenesis := types.GenesisState{ - Params: types.DefaultParams(), + //Params: types.DefaultParams(), + AirdropSupply: sdk.NewCoin("foo", sdk.ZeroInt()), // this line is used by starport scaffolding # simapp/module/genesisState } simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(&claimGenesis) diff --git a/x/claim/types/claim_record.go b/x/claim/types/claim_record.go index 5639f1eb0..9a0d9818f 100644 --- a/x/claim/types/claim_record.go +++ b/x/claim/types/claim_record.go @@ -27,3 +27,13 @@ func (m ClaimRecord) Validate() error { return nil } + +// IsMissionCompleted checks if the specified mission ID is completed for the claim record +func (m ClaimRecord) IsMissionCompleted(missionID uint64) bool { + for _, completed := range m.CompletedMissions { + if completed == missionID { + return true + } + } + return false +} diff --git a/x/claim/types/claim_record_test.go b/x/claim/types/claim_record_test.go index a32ebe931..6ca30ec27 100644 --- a/x/claim/types/claim_record_test.go +++ b/x/claim/types/claim_record_test.go @@ -21,6 +21,15 @@ func TestClaimRecord_Validate(t *testing.T) { claimRecord: sample.ClaimRecord(r), valid: true, }, + { + desc: "claim record with no completed mission is valid", + claimRecord: claim.ClaimRecord{ + Address: sample.Address(r), + Claimable: sdk.OneInt(), + CompletedMissions: []uint64{}, + }, + valid: true, + }, { desc: "should prevent invalid address", claimRecord: claim.ClaimRecord{ @@ -63,3 +72,29 @@ func TestClaimRecord_Validate(t *testing.T) { }) } } + +func TestClaimRecord_IsMissionCompleted(t *testing.T) { + require.False(t, claim.ClaimRecord{ + Address: sample.Address(r), + Claimable: sdk.OneInt(), + CompletedMissions: []uint64{}, + }.IsMissionCompleted(0)) + + require.False(t, claim.ClaimRecord{ + Address: sample.Address(r), + Claimable: sdk.OneInt(), + CompletedMissions: []uint64{1, 2, 3}, + }.IsMissionCompleted(0)) + + require.True(t, claim.ClaimRecord{ + Address: sample.Address(r), + Claimable: sdk.OneInt(), + CompletedMissions: []uint64{0, 1, 2, 3}, + }.IsMissionCompleted(0)) + + require.True(t, claim.ClaimRecord{ + Address: sample.Address(r), + Claimable: sdk.OneInt(), + CompletedMissions: []uint64{0, 1, 2, 3}, + }.IsMissionCompleted(3)) +} diff --git a/x/claim/types/errors.go b/x/claim/types/errors.go index d623849fd..edba6daf2 100644 --- a/x/claim/types/errors.go +++ b/x/claim/types/errors.go @@ -8,5 +8,8 @@ import ( // x/claim module sentinel errors var ( - ErrSample = sdkerrors.Register(ModuleName, 1100, "sample error") + ErrMissionNotFound = sdkerrors.Register(ModuleName, 2, "mission not found") + ErrClaimRecordNotFound = sdkerrors.Register(ModuleName, 3, "claim record not found") + ErrMissionCompleted = sdkerrors.Register(ModuleName, 4, "mission already completed") + ErrAirdropSupplyNotFound = sdkerrors.Register(ModuleName, 5, "airdrop supply not found") ) diff --git a/x/claim/types/expected_keepers.go b/x/claim/types/expected_keepers.go index 6aa6e9778..ed7483bbf 100644 --- a/x/claim/types/expected_keepers.go +++ b/x/claim/types/expected_keepers.go @@ -7,12 +7,15 @@ import ( // AccountKeeper defines the expected account keeper used for simulations (noalias) type AccountKeeper interface { + GetModuleAddress(name string) sdk.AccAddress GetAccount(ctx sdk.Context, addr sdk.AccAddress) types.AccountI - // Methods imported from account should be defined here } // BankKeeper defines the expected interface needed to retrieve account balances. type BankKeeper interface { SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins - // Methods imported from bank should be defined here + GetBalance(ctx sdk.Context, addr sdk.AccAddress, denom string) sdk.Coin + MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error }