diff --git a/proto/claim/params.proto b/proto/claim/params.proto index b3eccb215..866631f36 100644 --- a/proto/claim/params.proto +++ b/proto/claim/params.proto @@ -2,10 +2,20 @@ syntax = "proto3"; package tendermint.spn.claim; import "gogoproto/gogo.proto"; +import "google/protobuf/timestamp.proto"; option go_package = "github.com/tendermint/spn/x/claim/types"; // Params defines the parameters for the module. message Params { + DecayInformation decayInformation = 1 [(gogoproto.nullable) = false]; option (gogoproto.goproto_stringer) = false; } + +// DecayInformation defines the information about decay for the airdrop +// when claimable airdrop amount starts to decrease and when it ends +message DecayInformation { + bool enabled = 1; + google.protobuf.Timestamp decayStart = 2 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true]; + google.protobuf.Timestamp decayEnd = 3 [(gogoproto.nullable) = false, (gogoproto.stdtime) = true]; +} diff --git a/testutil/gen_app.go b/testutil/gen_app.go index 6a2d73cac..913b9efb9 100644 --- a/testutil/gen_app.go +++ b/testutil/gen_app.go @@ -13,11 +13,12 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - spnapp "github.com/tendermint/spn/app" - "github.com/tendermint/spn/cmd" "github.com/tendermint/tendermint/libs/log" tmtypes "github.com/tendermint/tendermint/types" dbm "github.com/tendermint/tm-db" + + spnapp "github.com/tendermint/spn/app" + "github.com/tendermint/spn/cmd" ) func GenApp(withGenesis bool, invCheckPeriod uint) (*spnapp.App, spnapp.GenesisState) { diff --git a/testutil/keeper/initializer.go b/testutil/keeper/initializer.go index 36b68fced..3f63bae90 100644 --- a/testutil/keeper/initializer.go +++ b/testutil/keeper/initializer.go @@ -483,7 +483,7 @@ func (i initializer) Claim( i.StateStore.MountStoreWithDB(memStoreKey, storetypes.StoreTypeMemory, nil) paramKeeper.Subspace(claimtypes.ModuleName) - subspace, _ := paramKeeper.GetSubspace(participationtypes.ModuleName) + subspace, _ := paramKeeper.GetSubspace(claimtypes.ModuleName) return claimkeeper.NewKeeper( i.Codec, diff --git a/x/claim/keeper/mission.go b/x/claim/keeper/mission.go index 5f46b3699..fe264d693 100644 --- a/x/claim/keeper/mission.go +++ b/x/claim/keeper/mission.go @@ -93,8 +93,17 @@ func (k Keeper) CompleteMission(ctx sdk.Context, missionID uint64, address strin claimableAmount := claimRecord.ClaimableFromMission(mission) claimable := sdk.NewCoins(sdk.NewCoin(airdropSupply.Denom, claimableAmount)) + // calculate claimable after decay factor + decayInfo := k.DecayInformation(ctx) + claimable = decayInfo.ApplyDecayFactor(claimable, ctx.BlockTime()) + + // check final claimable non-zero + if claimable.Empty() { + return types.ErrNoClaimable + } + // decrease airdrop supply - airdropSupply.Amount = airdropSupply.Amount.Sub(claimableAmount) + airdropSupply.Amount = airdropSupply.Amount.Sub(claimable.AmountOf(airdropSupply.Denom)) if airdropSupply.Amount.IsNegative() { return spnerrors.Critical("airdrop supply is lower than total claimable") } diff --git a/x/claim/keeper/mission_test.go b/x/claim/keeper/mission_test.go index 0d5f415fc..0e0fe48c7 100644 --- a/x/claim/keeper/mission_test.go +++ b/x/claim/keeper/mission_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" @@ -70,6 +71,8 @@ func TestKeeper_CompleteMission(t *testing.T) { airdropSupply sdk.Coin mission types.Mission claimRecord types.ClaimRecord + params types.Params + blockTime time.Time } // prepare addresses @@ -92,6 +95,7 @@ func TestKeeper_CompleteMission(t *testing.T) { noAirdropSupply: true, claimRecord: sample.ClaimRecord(r), mission: sample.Mission(r), + params: types.DefaultParams(), }, missionID: 1, address: sample.Address(r), @@ -107,6 +111,7 @@ func TestKeeper_CompleteMission(t *testing.T) { Claimable: sdk.OneInt(), CompletedMissions: []uint64{1}, }, + params: types.DefaultParams(), }, missionID: 1, address: addr[0], @@ -121,6 +126,7 @@ func TestKeeper_CompleteMission(t *testing.T) { MissionID: 1, Weight: sdk.OneDec(), }, + params: types.DefaultParams(), }, missionID: 1, address: sample.Address(r), @@ -139,6 +145,7 @@ func TestKeeper_CompleteMission(t *testing.T) { Claimable: sdk.OneInt(), CompletedMissions: []uint64{1}, }, + params: types.DefaultParams(), }, missionID: 1, address: addr[1], @@ -156,6 +163,7 @@ func TestKeeper_CompleteMission(t *testing.T) { Address: addr[2], Claimable: sdk.NewIntFromUint64(10000), }, + params: types.DefaultParams(), }, missionID: 1, address: addr[2], @@ -173,6 +181,7 @@ func TestKeeper_CompleteMission(t *testing.T) { Address: "invalid", Claimable: sdk.OneInt(), }, + params: types.DefaultParams(), }, missionID: 1, address: "invalid", @@ -190,13 +199,14 @@ func TestKeeper_CompleteMission(t *testing.T) { Address: addr[3], Claimable: sdk.NewIntFromUint64(1000), }, + params: types.DefaultParams(), }, missionID: 1, address: addr[3], expectedBalance: tc.Coin(t, "1000foo"), }, { - name: "should allow distributing no fund for mission with 0 weight", + name: "should prevent distributing fund for mission with 0 weight", inputState: inputState{ airdropSupply: tc.Coin(t, "1000foo"), mission: types.Mission{ @@ -207,10 +217,11 @@ func TestKeeper_CompleteMission(t *testing.T) { Address: addr[4], Claimable: sdk.NewIntFromUint64(1000), }, + params: types.DefaultParams(), }, - missionID: 1, - address: addr[4], - expectedBalance: tc.Coin(t, "0foo"), + missionID: 1, + address: addr[4], + err: types.ErrNoClaimable, }, { name: "should allow distributing half for mission with 0.5 weight", @@ -224,6 +235,7 @@ func TestKeeper_CompleteMission(t *testing.T) { Address: addr[5], Claimable: sdk.NewIntFromUint64(500), }, + params: types.DefaultParams(), }, missionID: 1, address: addr[5], @@ -241,13 +253,14 @@ func TestKeeper_CompleteMission(t *testing.T) { Address: addr[6], Claimable: sdk.NewIntFromUint64(201), }, + params: types.DefaultParams(), }, missionID: 1, address: addr[6], expectedBalance: tc.Coin(t, "100foo"), }, { - name: "should allow distributing no fund for empty claim record", + name: "should prevent distributing fund for empty claim record", inputState: inputState{ airdropSupply: tc.Coin(t, "1000foo"), mission: types.Mission{ @@ -258,10 +271,11 @@ func TestKeeper_CompleteMission(t *testing.T) { Address: addr[7], Claimable: sdk.ZeroInt(), }, + params: types.DefaultParams(), }, - missionID: 1, - address: addr[7], - expectedBalance: tc.Coin(t, "0foo"), + missionID: 1, + address: addr[7], + err: types.ErrNoClaimable, }, { name: "should allow distributing airdrop with other already completed missions", @@ -276,15 +290,84 @@ func TestKeeper_CompleteMission(t *testing.T) { Claimable: sdk.NewIntFromUint64(10000), CompletedMissions: []uint64{0, 1, 2, 4, 5, 6}, }, + params: types.DefaultParams(), }, missionID: 3, address: addr[8], expectedBalance: tc.Coin(t, "3000bar"), }, + { + name: "should allow applying decay factor if enabled", + inputState: inputState{ + airdropSupply: tc.Coin(t, "1000foo"), + mission: types.Mission{ + MissionID: 1, + Weight: tc.Dec(t, "0.5"), + }, + claimRecord: types.ClaimRecord{ + Address: addr[9], + Claimable: sdk.NewIntFromUint64(1000), + }, + params: types.NewParams(types.NewEnabledDecay( + time.Unix(1000, 0), + time.Unix(2000, 0), + )), + blockTime: time.Unix(1500, 0), + }, + missionID: 1, + address: addr[9], + expectedBalance: tc.Coin(t, "250foo"), + }, + { + name: "should allow distributing all funds if decay factor if enabled and decay not started", + inputState: inputState{ + airdropSupply: tc.Coin(t, "1000foo"), + mission: types.Mission{ + MissionID: 1, + Weight: tc.Dec(t, "0.5"), + }, + claimRecord: types.ClaimRecord{ + Address: addr[10], + Claimable: sdk.NewIntFromUint64(1000), + }, + params: types.NewParams(types.NewEnabledDecay( + time.Unix(1000, 0), + time.Unix(2000, 0), + )), + blockTime: time.Unix(999, 0), + }, + missionID: 1, + address: addr[10], + expectedBalance: tc.Coin(t, "500foo"), + }, + { + name: "should prevent distributing funds if decay ended", + inputState: inputState{ + airdropSupply: tc.Coin(t, "1000foo"), + mission: types.Mission{ + MissionID: 1, + Weight: tc.Dec(t, "0.5"), + }, + claimRecord: types.ClaimRecord{ + Address: addr[11], + Claimable: sdk.NewIntFromUint64(1000), + }, + params: types.NewParams(types.NewEnabledDecay( + time.Unix(1000, 0), + time.Unix(2000, 0), + )), + blockTime: time.Unix(2001, 0), + }, + missionID: 1, + address: addr[11], + err: types.ErrNoClaimable, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // initialize input state + require.NoError(t, tt.inputState.params.Validate()) + tk.ClaimKeeper.SetParams(ctx, tt.inputState.params) if !tt.inputState.noAirdropSupply { err := tk.ClaimKeeper.InitializeAirdropSupply(ctx, tt.inputState.airdropSupply) require.NoError(t, err) @@ -295,6 +378,9 @@ func TestKeeper_CompleteMission(t *testing.T) { if !tt.inputState.noClaimRecord { tk.ClaimKeeper.SetClaimRecord(ctx, tt.inputState.claimRecord) } + if !tt.inputState.blockTime.IsZero() { + ctx = ctx.WithBlockTime(tt.inputState.blockTime) + } err := tk.ClaimKeeper.CompleteMission(ctx, tt.missionID, tt.address) if tt.err != nil { diff --git a/x/claim/keeper/params.go b/x/claim/keeper/params.go index 4a910cc00..b6e0d444e 100644 --- a/x/claim/keeper/params.go +++ b/x/claim/keeper/params.go @@ -7,11 +7,18 @@ import ( ) // GetParams get all parameters as types.Params -func (k Keeper) GetParams(ctx sdk.Context) types.Params { - return types.NewParams() +func (k Keeper) GetParams(ctx sdk.Context) (params types.Params) { + k.paramstore.GetParamSet(ctx, ¶ms) + return params } // SetParams set the params func (k Keeper) SetParams(ctx sdk.Context, params types.Params) { k.paramstore.SetParamSet(ctx, ¶ms) } + +// DecayInformation returns the param that defines decay information +func (k Keeper) DecayInformation(ctx sdk.Context) (totalSupplyRange types.DecayInformation) { + k.paramstore.Get(ctx, types.KeyDecayInformation, &totalSupplyRange) + return +} diff --git a/x/claim/keeper/params_test.go b/x/claim/keeper/params_test.go index 1e12e51d0..dbaca897d 100644 --- a/x/claim/keeper/params_test.go +++ b/x/claim/keeper/params_test.go @@ -2,6 +2,7 @@ package keeper_test import ( "testing" + "time" "github.com/stretchr/testify/require" @@ -12,9 +13,10 @@ import ( func TestGetParams(t *testing.T) { ctx, tk, _ := testkeeper.NewTestSetup(t) - params := types.DefaultParams() - + params := types.NewParams(types.NewEnabledDecay( + time.Unix(1000, 0), + time.Unix(10000, 0), + )) tk.ClaimKeeper.SetParams(ctx, params) - require.EqualValues(t, params, tk.ClaimKeeper.GetParams(ctx)) } diff --git a/x/claim/types/decay.go b/x/claim/types/decay.go new file mode 100644 index 000000000..8c2315869 --- /dev/null +++ b/x/claim/types/decay.go @@ -0,0 +1,77 @@ +package types + +import ( + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// NewDisabledDecay returns decay information with disabled decay +func NewDisabledDecay() DecayInformation { + // convert all time to UTC + return DecayInformation{ + Enabled: false, + DecayStart: time.UnixMilli(0).UTC(), + DecayEnd: time.UnixMilli(0).UTC(), + } +} + +// NewEnabledDecay returns decay information with a decay start and end +func NewEnabledDecay(start, end time.Time) DecayInformation { + // convert all time to UTC + return DecayInformation{ + Enabled: true, + DecayStart: start.UTC(), + DecayEnd: end.UTC(), + } +} + +// Validate validates the decay information +func (m DecayInformation) Validate() error { + if m.Enabled && m.DecayStart.After(m.DecayEnd) { + return fmt.Errorf("decay starts after decay end %s > %s", m.DecayStart.String(), m.DecayEnd.String()) + } + + return nil +} + +// ApplyDecayFactor reduces the coins depending on the decay factor from decay information +// coins decrease from decay start to zero at decay end +func (m DecayInformation) ApplyDecayFactor(coins sdk.Coins, currentTime time.Time) sdk.Coins { + // no decay factor applied + if coins.Empty() || !m.Enabled || currentTime.Before(m.DecayStart) { + return coins + } + + // coins reduced to 0 if decay ended + if currentTime.Equal(m.DecayEnd) || currentTime.After(m.DecayEnd) { + return sdk.NewCoins() + } + + // calculate decay factor + timeToDec := func(t time.Time) sdk.Dec { + return sdk.NewDecFromInt(sdk.NewInt(t.Unix())) + } + + current, start, end := timeToDec(currentTime), timeToDec(m.DecayStart), timeToDec(m.DecayEnd) + + // (end-current)/(end-start) + decayFactor := (end.Sub(current)).Quo(end.Sub(start)) + + // apply decay factor to each denom + newCoins := sdk.NewCoins() + for _, coin := range coins { + amountDec := sdk.NewDecFromInt(coin.Amount) + newAmount := amountDec.Mul(decayFactor).TruncateInt() + + if !newAmount.IsZero() { + newCoins = append(newCoins, sdk.NewCoin( + coin.Denom, + newAmount, + )) + } + } + + return newCoins.Sort() +} diff --git a/x/claim/types/decay_test.go b/x/claim/types/decay_test.go new file mode 100644 index 000000000..ab68b2bb0 --- /dev/null +++ b/x/claim/types/decay_test.go @@ -0,0 +1,208 @@ +package types_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + tc "github.com/tendermint/spn/testutil/constructor" + "github.com/tendermint/spn/x/claim/types" +) + +func TestNewDisabledDecay(t *testing.T) { + decayInfo := types.NewDisabledDecay() + require.False(t, decayInfo.Enabled) +} + +func TestNewEnabledDecay(t *testing.T) { + start := time.UnixMilli(1000) + end := time.UnixMilli(2000) + decayInfo := types.NewEnabledDecay(start, end) + require.True(t, decayInfo.Enabled) + require.True(t, decayInfo.DecayStart.Equal(start)) + require.True(t, decayInfo.DecayEnd.Equal(end)) +} + +func TestDecayInformation_Validate(t *testing.T) { + tests := []struct { + name string + decayInfo types.DecayInformation + wantErr bool + }{ + { + name: "should validate decay information with disabled", + decayInfo: types.DecayInformation{ + Enabled: false, + DecayStart: time.UnixMilli(2000), + DecayEnd: time.UnixMilli(1000), + }, + }, + { + name: "should validate decay information with enabled and start equals to end", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.UnixMilli(1000), + DecayEnd: time.UnixMilli(1000), + }, + }, + { + name: "should validate decay information with enabled and end greater than start", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.UnixMilli(1000), + DecayEnd: time.UnixMilli(10000), + }, + }, + { + name: "should prevent validate decay information with enabled and start greater than end", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.UnixMilli(1001), + DecayEnd: time.UnixMilli(1000), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.decayInfo.Validate() + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestDecayInformation_ApplyDecayFactor(t *testing.T) { + tests := []struct { + name string + decayInfo types.DecayInformation + coins sdk.Coins + currentTime time.Time + expectedCoins sdk.Coins + }{ + { + name: "should apply no change if decay disabled", + decayInfo: types.DecayInformation{ + Enabled: false, + }, + coins: tc.Coins(t, "100foo,100bar"), + expectedCoins: tc.Coins(t, "100foo,100bar"), + }, + { + name: "should apply no change if decay not started", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.Unix(1000, 0), + DecayEnd: time.Unix(10000, 0), + }, + currentTime: time.Unix(500, 0), + coins: tc.Coins(t, "100foo,100bar"), + expectedCoins: tc.Coins(t, "100foo,100bar"), + }, + { + name: "should return zero coins if end of decay", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.Unix(1000, 0), + DecayEnd: time.Unix(10000, 0), + }, + currentTime: time.Unix(10000, 0), + coins: tc.Coins(t, "100foo,100bar"), + expectedCoins: sdk.NewCoins(), + }, + { + name: "should return zero coins if end of decay without start", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.Unix(10000, 0), + DecayEnd: time.Unix(10000, 0), + }, + currentTime: time.Unix(10000, 0), + coins: tc.Coins(t, "100foo,100bar"), + expectedCoins: sdk.NewCoins(), + }, + { + name: "should return zero coins if decay ended", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.Unix(1000, 0), + DecayEnd: time.Unix(10000, 0), + }, + currentTime: time.Unix(10001, 0), + coins: tc.Coins(t, "100foo,100bar"), + expectedCoins: sdk.NewCoins(), + }, + { + name: "should apply half decay factor", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.Unix(10000, 0), + DecayEnd: time.Unix(20000, 0), + }, + currentTime: time.Unix(15000, 0), + coins: tc.Coins(t, "200000foo,2000000bar"), + expectedCoins: tc.Coins(t, "100000foo,1000000bar"), + }, + { + name: "should apply 0.6 decay factor", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.Unix(10000, 0), + DecayEnd: time.Unix(20000, 0), + }, + currentTime: time.Unix(14000, 0), + coins: tc.Coins(t, "100000foo,1000000bar"), + expectedCoins: tc.Coins(t, "60000foo,600000bar"), + }, + { + name: "should apply 0.2 decay factor", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.Unix(10000, 0), + DecayEnd: time.Unix(20000, 0), + }, + currentTime: time.Unix(18000, 0), + coins: tc.Coins(t, "100000foo,1000000bar"), + expectedCoins: tc.Coins(t, "20000foo,200000bar"), + }, + { + name: "should apply decay factor and truncate decimals", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.Unix(10000, 0), + DecayEnd: time.Unix(20000, 0), + }, + currentTime: time.Unix(15000, 0), + coins: tc.Coins(t, "100000foo,1bar,1000000000003baz"), + expectedCoins: tc.Coins(t, "50000foo,500000000001baz"), + }, + { + name: "should return ze coins if factor applied to zero coins", + decayInfo: types.DecayInformation{ + Enabled: true, + DecayStart: time.Unix(10000, 0), + DecayEnd: time.Unix(20000, 0), + }, + currentTime: time.Unix(15000, 0), + coins: sdk.NewCoins(), + expectedCoins: sdk.NewCoins(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newCoins := tt.decayInfo.ApplyDecayFactor(tt.coins, tt.currentTime) + + require.True(t, newCoins.IsEqual(tt.expectedCoins), + "new coins are not equal to expected coins, %s != %s", + newCoins.String(), + tt.expectedCoins.String(), + ) + }) + } +} diff --git a/x/claim/types/errors.go b/x/claim/types/errors.go index 53da14b77..fd224d13a 100644 --- a/x/claim/types/errors.go +++ b/x/claim/types/errors.go @@ -15,4 +15,5 @@ var ( ErrInitialClaimNotFound = sdkerrors.Register(ModuleName, 6, "initial claim information not found") ErrInitialClaimNotEnabled = sdkerrors.Register(ModuleName, 7, "initial claim not enabled") ErrMissionCompleteFailure = sdkerrors.Register(ModuleName, 8, "mission failed to complete") + ErrNoClaimable = sdkerrors.Register(ModuleName, 9, "no amount to be claimed") ) diff --git a/x/claim/types/genesis_test.go b/x/claim/types/genesis_test.go index f2737a200..a1f360011 100644 --- a/x/claim/types/genesis_test.go +++ b/x/claim/types/genesis_test.go @@ -2,6 +2,7 @@ package types_test import ( "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" @@ -33,6 +34,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should validate airdrop supply sum of claim amounts", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -65,6 +67,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should allow genesis state with no airdrop supply", genState: &types.GenesisState{ + Params: types.DefaultParams(), Missions: []types.Mission{ { MissionID: 0, @@ -78,6 +81,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should allow genesis state with no mission", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -95,6 +99,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should allow mission with 0 weight", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -122,6 +127,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should allow claim record with completed missions", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -151,6 +157,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should allow claim record with missions all completed", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -180,6 +187,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should allow claim record with zero weight mission completed", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -209,6 +217,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should validate genesis state with initial claim enabled", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -236,6 +245,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should prevent validate duplicated claimRecord", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: "duplicate", @@ -259,6 +269,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should prevent validate claim record with non positive allocation", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -282,6 +293,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should prevent validate airdrop supply higher than sum of claim amounts", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -305,6 +317,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should prevent validate airdrop supply lower than sum of claim amounts", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -328,6 +341,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should prevent validate invalid airdrop supply with records with completed missions", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -356,6 +370,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should prevent validate claim record with non existing mission", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -380,6 +395,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should prevent validate invalid genesis supply coin", genState: &types.GenesisState{ + Params: types.DefaultParams(), AirdropSupply: sdk.Coin{}, Missions: []types.Mission{ { @@ -393,6 +409,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should prevent validate duplicated mission", genState: &types.GenesisState{ + Params: types.DefaultParams(), Missions: []types.Mission{ { MissionID: 0, @@ -409,6 +426,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should prevent validate mission list weights are not equal to 1", genState: &types.GenesisState{ + Params: types.DefaultParams(), Missions: []types.Mission{ { MissionID: 0, @@ -425,6 +443,7 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "should prevent validate initial claim enabled with non existing mission", genState: &types.GenesisState{ + Params: types.DefaultParams(), ClaimRecords: []types.ClaimRecord{ { Address: sample.Address(r), @@ -443,6 +462,24 @@ func TestGenesisState_Validate(t *testing.T) { }, valid: false, }, + { + desc: "should prevent validate genesis state with invalid param", + genState: &types.GenesisState{ + Params: types.NewParams(types.DecayInformation{ + Enabled: true, + DecayStart: time.UnixMilli(1001), + DecayEnd: time.UnixMilli(1000), + }), + Missions: []types.Mission{ + { + MissionID: 0, + Weight: sdk.OneDec(), + }, + }, + AirdropSupply: tc.Coin(t, "0foo"), + }, + valid: false, + }, // this line is used by starport scaffolding # types/genesis/testcase } { t.Run(tt.desc, func(t *testing.T) { diff --git a/x/claim/types/params.go b/x/claim/types/params.go index 357196ad6..29cf213bc 100644 --- a/x/claim/types/params.go +++ b/x/claim/types/params.go @@ -1,35 +1,55 @@ package types import ( + "fmt" + paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "gopkg.in/yaml.v2" ) var _ paramtypes.ParamSet = (*Params)(nil) +var ( + KeyDecayInformation = []byte("DecayInformation") +) + // ParamKeyTable the param key table for launch module func ParamKeyTable() paramtypes.KeyTable { return paramtypes.NewKeyTable().RegisterParamSet(&Params{}) } // NewParams creates a new Params instance -func NewParams() Params { - return Params{} +func NewParams(di DecayInformation) Params { + return Params{ + DecayInformation: di, + } } // DefaultParams returns a default set of parameters func DefaultParams() Params { - return NewParams() + return NewParams(NewDisabledDecay()) } // ParamSetPairs get the params.ParamSet func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { - return paramtypes.ParamSetPairs{} + return paramtypes.ParamSetPairs{ + paramtypes.NewParamSetPair(KeyDecayInformation, &p.DecayInformation, validateDecayInformation), + } } // Validate validates the set of params func (p Params) Validate() error { - return nil + return validateDecayInformation(p.DecayInformation) +} + +// validateDecayInformation validates the DecayInformation param +func validateDecayInformation(v interface{}) error { + decayInfo, ok := v.(DecayInformation) + if !ok { + return fmt.Errorf("invalid parameter type: %T", v) + } + + return decayInfo.Validate() } // String implements the Stringer interface. diff --git a/x/claim/types/params.pb.go b/x/claim/types/params.pb.go index 876e86956..c350a4d76 100644 --- a/x/claim/types/params.pb.go +++ b/x/claim/types/params.pb.go @@ -7,15 +7,19 @@ import ( fmt "fmt" _ "github.com/gogo/protobuf/gogoproto" proto "github.com/gogo/protobuf/proto" + github_com_gogo_protobuf_types "github.com/gogo/protobuf/types" + _ "google.golang.org/protobuf/types/known/timestamppb" io "io" math "math" math_bits "math/bits" + time "time" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf +var _ = time.Kitchen // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. @@ -25,6 +29,7 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package // Params defines the parameters for the module. type Params struct { + DecayInformation DecayInformation `protobuf:"bytes,1,opt,name=decayInformation,proto3" json:"decayInformation"` } func (m *Params) Reset() { *m = Params{} } @@ -59,24 +64,103 @@ func (m *Params) XXX_DiscardUnknown() { var xxx_messageInfo_Params proto.InternalMessageInfo +func (m *Params) GetDecayInformation() DecayInformation { + if m != nil { + return m.DecayInformation + } + return DecayInformation{} +} + +// DecayInformation defines the information about decay for the airdrop +// when claimable airdrop amount starts to decrease and when it ends +type DecayInformation struct { + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` + DecayStart time.Time `protobuf:"bytes,2,opt,name=decayStart,proto3,stdtime" json:"decayStart"` + DecayEnd time.Time `protobuf:"bytes,3,opt,name=decayEnd,proto3,stdtime" json:"decayEnd"` +} + +func (m *DecayInformation) Reset() { *m = DecayInformation{} } +func (m *DecayInformation) String() string { return proto.CompactTextString(m) } +func (*DecayInformation) ProtoMessage() {} +func (*DecayInformation) Descriptor() ([]byte, []int) { + return fileDescriptor_adbfc9fc41f7a9d2, []int{1} +} +func (m *DecayInformation) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *DecayInformation) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_DecayInformation.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *DecayInformation) XXX_Merge(src proto.Message) { + xxx_messageInfo_DecayInformation.Merge(m, src) +} +func (m *DecayInformation) XXX_Size() int { + return m.Size() +} +func (m *DecayInformation) XXX_DiscardUnknown() { + xxx_messageInfo_DecayInformation.DiscardUnknown(m) +} + +var xxx_messageInfo_DecayInformation proto.InternalMessageInfo + +func (m *DecayInformation) GetEnabled() bool { + if m != nil { + return m.Enabled + } + return false +} + +func (m *DecayInformation) GetDecayStart() time.Time { + if m != nil { + return m.DecayStart + } + return time.Time{} +} + +func (m *DecayInformation) GetDecayEnd() time.Time { + if m != nil { + return m.DecayEnd + } + return time.Time{} +} + func init() { proto.RegisterType((*Params)(nil), "tendermint.spn.claim.Params") + proto.RegisterType((*DecayInformation)(nil), "tendermint.spn.claim.DecayInformation") } func init() { proto.RegisterFile("claim/params.proto", fileDescriptor_adbfc9fc41f7a9d2) } var fileDescriptor_adbfc9fc41f7a9d2 = []byte{ - // 151 bytes of a gzipped FileDescriptorProto + // 300 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4a, 0xce, 0x49, 0xcc, 0xcc, 0xd5, 0x2f, 0x48, 0x2c, 0x4a, 0xcc, 0x2d, 0xd6, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x29, 0x49, 0xcd, 0x4b, 0x49, 0x2d, 0xca, 0xcd, 0xcc, 0x2b, 0xd1, 0x2b, 0x2e, 0xc8, 0xd3, 0x03, - 0x2b, 0x91, 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0x2b, 0xd0, 0x07, 0xb1, 0x20, 0x6a, 0x95, 0xf8, - 0xb8, 0xd8, 0x02, 0xc0, 0x7a, 0xad, 0x58, 0x66, 0x2c, 0x90, 0x67, 0x70, 0x72, 0x3c, 0xf1, 0x48, - 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, - 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, 0xf5, 0xf4, 0xcc, 0x92, 0x8c, 0xd2, 0x24, 0xbd, - 0xe4, 0xfc, 0x5c, 0x7d, 0x84, 0x05, 0xfa, 0xc5, 0x05, 0x79, 0xfa, 0x15, 0xfa, 0x10, 0x57, 0x94, - 0x54, 0x16, 0xa4, 0x16, 0x27, 0xb1, 0x81, 0x4d, 0x36, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xa9, - 0x90, 0x93, 0xcf, 0x9b, 0x00, 0x00, 0x00, + 0x2b, 0x91, 0x12, 0x49, 0xcf, 0x4f, 0xcf, 0x07, 0x2b, 0xd0, 0x07, 0xb1, 0x20, 0x6a, 0xa5, 0xe4, + 0xd3, 0xf3, 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0xbc, 0xa4, 0xd2, 0x34, 0xfd, 0x92, 0xcc, 0xdc, + 0xd4, 0xe2, 0x92, 0xc4, 0xdc, 0x02, 0x88, 0x02, 0xa5, 0x0c, 0x2e, 0xb6, 0x00, 0xb0, 0xe1, 0x42, + 0x11, 0x5c, 0x02, 0x29, 0xa9, 0xc9, 0x89, 0x95, 0x9e, 0x79, 0x69, 0xf9, 0x45, 0xb9, 0x89, 0x25, + 0x99, 0xf9, 0x79, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0xdc, 0x46, 0x6a, 0x7a, 0xd8, 0x6c, 0xd4, 0x73, + 0x41, 0x53, 0xed, 0xc4, 0x72, 0xe2, 0x9e, 0x3c, 0x43, 0x10, 0x86, 0x29, 0x56, 0x2c, 0x33, 0x16, + 0xc8, 0x33, 0x28, 0x6d, 0x61, 0xe4, 0x12, 0x40, 0xd7, 0x22, 0x24, 0xc1, 0xc5, 0x9e, 0x9a, 0x97, + 0x98, 0x94, 0x93, 0x9a, 0x02, 0xb6, 0x8b, 0x23, 0x08, 0xc6, 0x15, 0x72, 0xe1, 0xe2, 0x02, 0x1b, + 0x14, 0x5c, 0x92, 0x58, 0x54, 0x22, 0xc1, 0x04, 0x76, 0x88, 0x94, 0x1e, 0xc4, 0x3b, 0x7a, 0x30, + 0xef, 0xe8, 0x85, 0xc0, 0xbc, 0xe3, 0xc4, 0x01, 0xb2, 0x7c, 0xc2, 0x7d, 0x79, 0xc6, 0x20, 0x24, + 0x7d, 0x42, 0x0e, 0x5c, 0x1c, 0x60, 0x9e, 0x6b, 0x5e, 0x8a, 0x04, 0x33, 0x09, 0x66, 0xc0, 0x75, + 0x39, 0x39, 0x9e, 0x78, 0x24, 0xc7, 0x78, 0xe1, 0x91, 0x1c, 0xe3, 0x83, 0x47, 0x72, 0x8c, 0x13, + 0x1e, 0xcb, 0x31, 0x5c, 0x78, 0x2c, 0xc7, 0x70, 0xe3, 0xb1, 0x1c, 0x43, 0x94, 0x7a, 0x7a, 0x66, + 0x49, 0x46, 0x69, 0x92, 0x5e, 0x72, 0x7e, 0xae, 0x3e, 0x22, 0x80, 0xf4, 0x8b, 0x0b, 0xf2, 0xf4, + 0x2b, 0xf4, 0x21, 0xf1, 0x56, 0x52, 0x59, 0x90, 0x5a, 0x9c, 0xc4, 0x06, 0xb6, 0xca, 0x18, 0x10, + 0x00, 0x00, 0xff, 0xff, 0x64, 0x15, 0x99, 0xd0, 0xcd, 0x01, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -99,6 +183,65 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size, err := m.DecayInformation.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintParams(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func (m *DecayInformation) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *DecayInformation) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *DecayInformation) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + n2, err2 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.DecayEnd, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.DecayEnd):]) + if err2 != nil { + return 0, err2 + } + i -= n2 + i = encodeVarintParams(dAtA, i, uint64(n2)) + i-- + dAtA[i] = 0x1a + n3, err3 := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.DecayStart, dAtA[i-github_com_gogo_protobuf_types.SizeOfStdTime(m.DecayStart):]) + if err3 != nil { + return 0, err3 + } + i -= n3 + i = encodeVarintParams(dAtA, i, uint64(n3)) + i-- + dAtA[i] = 0x12 + if m.Enabled { + i-- + if m.Enabled { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x8 + } return len(dAtA) - i, nil } @@ -119,6 +262,24 @@ func (m *Params) Size() (n int) { } var l int _ = l + l = m.DecayInformation.Size() + n += 1 + l + sovParams(uint64(l)) + return n +} + +func (m *DecayInformation) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Enabled { + n += 2 + } + l = github_com_gogo_protobuf_types.SizeOfStdTime(m.DecayStart) + n += 1 + l + sovParams(uint64(l)) + l = github_com_gogo_protobuf_types.SizeOfStdTime(m.DecayEnd) + n += 1 + l + sovParams(uint64(l)) return n } @@ -157,6 +318,175 @@ func (m *Params) Unmarshal(dAtA []byte) error { return fmt.Errorf("proto: Params: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DecayInformation", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthParams + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthParams + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.DecayInformation.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipParams(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthParams + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *DecayInformation) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: DecayInformation: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: DecayInformation: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Enabled", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.Enabled = bool(v != 0) + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DecayStart", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthParams + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthParams + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.DecayStart, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DecayEnd", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowParams + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthParams + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthParams + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := github_com_gogo_protobuf_types.StdTimeUnmarshal(&m.DecayEnd, dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipParams(dAtA[iNdEx:]) diff --git a/x/claim/types/params_test.go b/x/claim/types/params_test.go new file mode 100644 index 000000000..f0cd551f9 --- /dev/null +++ b/x/claim/types/params_test.go @@ -0,0 +1,81 @@ +package types + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestParams_Validate(t *testing.T) { + tests := []struct { + name string + params Params + wantErr bool + }{ + { + name: "should prevent validate params with invalid decay information", + params: NewParams(DecayInformation{ + Enabled: true, + DecayStart: time.UnixMilli(1001), + DecayEnd: time.UnixMilli(1000), + }), + wantErr: true, + }, + { + name: "should validate valid params", + params: DefaultParams(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.params.Validate() + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestValidateDecayInformation(t *testing.T) { + tests := []struct { + name string + decayInformation interface{} + wantErr bool + }{ + { + name: "should prevent validate decay information with invalid interface", + decayInformation: "test", + wantErr: true, + }, + { + name: "should prevent validate invalid decay information", + decayInformation: DecayInformation{ + Enabled: true, + DecayStart: time.UnixMilli(1001), + DecayEnd: time.UnixMilli(1000), + }, + wantErr: true, + }, + { + name: "should validate valid decay information", + decayInformation: DecayInformation{ + Enabled: false, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateDecayInformation(tt.decayInformation) + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/x/mint/keeper/integration_test.go b/x/mint/keeper/integration_test.go index f1c14b0a8..34b7234d4 100644 --- a/x/mint/keeper/integration_test.go +++ b/x/mint/keeper/integration_test.go @@ -2,13 +2,15 @@ package keeper_test import ( "encoding/json" + "github.com/cosmos/cosmos-sdk/simapp" sdk "github.com/cosmos/cosmos-sdk/types" + abci "github.com/tendermint/tendermint/abci/types" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + spnapp "github.com/tendermint/spn/app" "github.com/tendermint/spn/testutil" "github.com/tendermint/spn/x/mint/types" - abci "github.com/tendermint/tendermint/abci/types" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" ) // returns context and an app with updated mint keeper