From be7242d86dd1cdf00029e9ffaaa4b7b1296ff474 Mon Sep 17 00:00:00 2001 From: Derrick Lee Date: Wed, 5 Oct 2022 11:39:50 -0700 Subject: [PATCH] Add `/incentive/apy` query endpoint (#1328) * Add /incentive/apy query endpoint * Add APY calculation using usd value * Fix maths for adding bkava incentive rewards Signed-off-by: drklee3 * Fix apy calculation with community tax, remove infra tax * Fix pricefeed market mismatches, fix division by zero * Remove unused kavadist expected keeper Signed-off-by: drklee3 --- app/app.go | 3 + x/incentive/client/rest/query.go | 21 +++ x/incentive/keeper/keeper.go | 33 ++-- x/incentive/keeper/querier.go | 163 ++++++++++++++++++++ x/incentive/keeper/querier_test.go | 102 ++++++++++++ x/incentive/keeper/unit_test.go | 213 +++++++++++++++++++++++++- x/incentive/types/expected_keepers.go | 19 +++ x/incentive/types/querier.go | 30 ++++ 8 files changed, 571 insertions(+), 13 deletions(-) create mode 100644 x/incentive/keeper/querier_test.go diff --git a/app/app.go b/app/app.go index 321d1194..d853d1d9 100644 --- a/app/app.go +++ b/app/app.go @@ -633,6 +633,9 @@ func NewApp( &savingsKeeper, &app.liquidKeeper, &earnKeeper, + app.mintKeeper, + app.distrKeeper, + app.pricefeedKeeper, ) app.routerKeeper = routerkeeper.NewKeeper( &app.earnKeeper, diff --git a/x/incentive/client/rest/query.go b/x/incentive/client/rest/query.go index 437b79ec..e1d6982d 100644 --- a/x/incentive/client/rest/query.go +++ b/x/incentive/client/rest/query.go @@ -19,6 +19,7 @@ func registerQueryRoutes(cliCtx client.Context, r *mux.Router) { r.HandleFunc(fmt.Sprintf("/%s/rewards", types.ModuleName), queryRewardsHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET") r.HandleFunc(fmt.Sprintf("/%s/reward-factors", types.ModuleName), queryRewardFactorsHandlerFn(cliCtx)).Methods("GET") + r.HandleFunc(fmt.Sprintf("/%s/apy", types.ModuleName), queryAPYsHandlerFn(cliCtx)).Methods("GET") } func queryRewardsHandlerFn(cliCtx client.Context) http.HandlerFunc { @@ -273,3 +274,23 @@ func executeAllRewardQueries(w http.ResponseWriter, cliCtx client.Context, param rest.PostProcessResponse(w, cliCtx, resBz) } + +func queryAPYsHandlerFn(cliCtx client.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r) + if !ok { + return + } + + route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetAPYs) + + res, height, err := cliCtx.QueryWithData(route, nil) + if err != nil { + rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error()) + return + } + + cliCtx = cliCtx.WithHeight(height) + rest.PostProcessResponse(w, cliCtx, res) + } +} diff --git a/x/incentive/keeper/keeper.go b/x/incentive/keeper/keeper.go index 15b5475a..1b90f971 100644 --- a/x/incentive/keeper/keeper.go +++ b/x/incentive/keeper/keeper.go @@ -24,6 +24,11 @@ type Keeper struct { savingsKeeper types.SavingsKeeper liquidKeeper types.LiquidKeeper earnKeeper types.EarnKeeper + + // Keepers used for APY queries + mintKeeper types.MintKeeper + distrKeeper types.DistrKeeper + pricefeedKeeper types.PricefeedKeeper } // NewKeeper creates a new keeper @@ -31,24 +36,28 @@ func NewKeeper( cdc codec.Codec, key sdk.StoreKey, paramstore types.ParamSubspace, bk types.BankKeeper, cdpk types.CdpKeeper, hk types.HardKeeper, ak types.AccountKeeper, stk types.StakingKeeper, swpk types.SwapKeeper, svk types.SavingsKeeper, lqk types.LiquidKeeper, ek types.EarnKeeper, + mk types.MintKeeper, dk types.DistrKeeper, pfk types.PricefeedKeeper, ) Keeper { if !paramstore.HasKeyTable() { paramstore = paramstore.WithKeyTable(types.ParamKeyTable()) } return Keeper{ - accountKeeper: ak, - cdc: cdc, - key: key, - paramSubspace: paramstore, - bankKeeper: bk, - cdpKeeper: cdpk, - hardKeeper: hk, - stakingKeeper: stk, - swapKeeper: swpk, - savingsKeeper: svk, - liquidKeeper: lqk, - earnKeeper: ek, + accountKeeper: ak, + cdc: cdc, + key: key, + paramSubspace: paramstore, + bankKeeper: bk, + cdpKeeper: cdpk, + hardKeeper: hk, + stakingKeeper: stk, + swapKeeper: swpk, + savingsKeeper: svk, + liquidKeeper: lqk, + earnKeeper: ek, + mintKeeper: mk, + distrKeeper: dk, + pricefeedKeeper: pfk, } } diff --git a/x/incentive/keeper/querier.go b/x/incentive/keeper/querier.go index 43af52f2..197ddd49 100644 --- a/x/incentive/keeper/querier.go +++ b/x/incentive/keeper/querier.go @@ -1,13 +1,21 @@ package keeper import ( + "fmt" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" abci "github.com/tendermint/tendermint/abci/types" + earntypes "github.com/kava-labs/kava/x/earn/types" "github.com/kava-labs/kava/x/incentive/types" + liquidtypes "github.com/kava-labs/kava/x/liquid/types" +) + +const ( + SecondsPerYear = 31536000 ) // NewQuerier is the module level router for state queries @@ -31,6 +39,8 @@ func NewQuerier(k Keeper, legacyQuerierCdc *codec.LegacyAmino) sdk.Querier { return queryGetRewardFactors(ctx, req, k, legacyQuerierCdc) case types.QueryGetEarnRewards: return queryGetEarnRewards(ctx, req, k, legacyQuerierCdc) + case types.QueryGetAPYs: + return queryGetAPYs(ctx, req, k, legacyQuerierCdc) default: return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName) } @@ -368,3 +378,156 @@ func queryGetRewardFactors(ctx sdk.Context, req abci.RequestQuery, k Keeper, leg return bz, nil } + +func queryGetAPYs(ctx sdk.Context, req abci.RequestQuery, k Keeper, legacyQuerierCdc *codec.LegacyAmino) ([]byte, error) { + params := k.GetParams(ctx) + var apys types.APYs + + // bkava APY (staking + incentive rewards) + stakingAPR, err := GetStakingAPR(ctx, k, params) + if err != nil { + return nil, err + } + + apys = append(apys, types.NewAPY(liquidtypes.DefaultDerivativeDenom, stakingAPR)) + + // Incentive only APYs + for _, param := range params.EarnRewardPeriods { + // Skip bkava as it's calculated earlier with staking rewards + if param.CollateralType == liquidtypes.DefaultDerivativeDenom { + continue + } + + // Value in the vault in the same denom as CollateralType + vaultTotalValue, err := k.earnKeeper.GetVaultTotalValue(ctx, param.CollateralType) + if err != nil { + return nil, err + } + apy, err := GetAPYFromMultiRewardPeriod(ctx, k, param.CollateralType, param, vaultTotalValue.Amount) + if err != nil { + return nil, err + } + + apys = append(apys, types.NewAPY(param.CollateralType, apy)) + } + + // Marshal APYs + res := types.NewQueryGetAPYsResponse(apys) + bz, err := codec.MarshalJSONIndent(legacyQuerierCdc, res) + if err != nil { + return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) + } + return bz, nil +} + +// GetStakingAPR returns the total APR for staking and incentive rewards +func GetStakingAPR(ctx sdk.Context, k Keeper, params types.Params) (sdk.Dec, error) { + // Get staking APR + incentive APR + inflationRate := k.mintKeeper.GetMinter(ctx).Inflation + communityTax := k.distrKeeper.GetCommunityTax(ctx) + + bondedTokens := k.stakingKeeper.TotalBondedTokens(ctx) + circulatingSupply := k.bankKeeper.GetSupply(ctx, types.BondDenom) + + // Staking APR = (Inflation Rate * (1 - Community Tax)) / (Bonded Tokens / Circulating Supply) + stakingAPR := inflationRate. + Mul(sdk.OneDec().Sub(communityTax)). + Quo(bondedTokens.ToDec(). + Quo(circulatingSupply.Amount.ToDec())) + + // Get incentive APR + bkavaRewardPeriod, found := params.EarnRewardPeriods.GetMultiRewardPeriod(liquidtypes.DefaultDerivativeDenom) + if !found { + // No incentive rewards for bkava, only staking rewards + return stakingAPR, nil + } + + // Total amount of bkava in earn vaults, this may be lower than total bank + // supply of bkava as some bkava may not be deposited in earn vaults + totalEarnBkavaDeposited := sdk.ZeroInt() + + var iterErr error + k.earnKeeper.IterateVaultRecords(ctx, func(record earntypes.VaultRecord) (stop bool) { + if !k.liquidKeeper.IsDerivativeDenom(ctx, record.TotalShares.Denom) { + return false + } + + vaultValue, err := k.earnKeeper.GetVaultTotalValue(ctx, record.TotalShares.Denom) + if err != nil { + iterErr = err + return false + } + + totalEarnBkavaDeposited = totalEarnBkavaDeposited.Add(vaultValue.Amount) + + return false + }) + + if iterErr != nil { + return sdk.ZeroDec(), iterErr + } + + // Incentive APR = rewards per second * seconds per year / total supplied to earn vaults + // Override collateral type to use "kava" instead of "bkava" when fetching + incentiveAPY, err := GetAPYFromMultiRewardPeriod(ctx, k, types.BondDenom, bkavaRewardPeriod, totalEarnBkavaDeposited) + if err != nil { + return sdk.ZeroDec(), err + } + + totalAPY := stakingAPR.Add(incentiveAPY) + return totalAPY, nil +} + +// GetAPYFromMultiRewardPeriod calculates the APY for a given MultiRewardPeriod +func GetAPYFromMultiRewardPeriod( + ctx sdk.Context, + k Keeper, + collateralType string, + rewardPeriod types.MultiRewardPeriod, + totalSupply sdk.Int, +) (sdk.Dec, error) { + if totalSupply.IsZero() { + return sdk.ZeroDec(), nil + } + + // Get USD value of collateral type + collateralUSDValue, err := k.pricefeedKeeper.GetCurrentPrice(ctx, getMarketID(collateralType)) + if err != nil { + return sdk.ZeroDec(), err + } + + // Total USD value of the collateral type total supply + totalSupplyUSDValue := totalSupply.ToDec().Mul(collateralUSDValue.Price) + + totalUSDRewardsPerSecond := sdk.ZeroDec() + + // In many cases, RewardsPerSecond are assets that are different from the + // CollateralType, so we need to use the USD value of CollateralType and + // RewardsPerSecond to determine the APY. + for _, reward := range rewardPeriod.RewardsPerSecond { + // Get USD value of 1 unit of reward asset type, using TWAP + rewardDenomUSDValue, err := k.pricefeedKeeper.GetCurrentPrice(ctx, getMarketID(reward.Denom)) + if err != nil { + return sdk.ZeroDec(), fmt.Errorf("failed to get price for RewardsPerSecond asset %s: %w", reward.Denom, err) + } + + rewardPerSecond := reward.Amount.ToDec().Mul(rewardDenomUSDValue.Price) + totalUSDRewardsPerSecond = totalUSDRewardsPerSecond.Add(rewardPerSecond) + } + + // APY = USD rewards per second * seconds per year / USD total supplied + apy := totalUSDRewardsPerSecond. + MulInt64(SecondsPerYear). + Quo(totalSupplyUSDValue) + + return apy, nil +} + +func getMarketID(denom string) string { + if denom == types.BondDenom { + // Rewrite "ukava" to "kava" as pricefeed only has "kava" and not "ukava" + return getMarketID("kava") + } + + return fmt.Sprintf("%s:usd:30", denom) +} diff --git a/x/incentive/keeper/querier_test.go b/x/incentive/keeper/querier_test.go new file mode 100644 index 00000000..a37725aa --- /dev/null +++ b/x/incentive/keeper/querier_test.go @@ -0,0 +1,102 @@ +package keeper_test + +import ( + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types" + + earntypes "github.com/kava-labs/kava/x/earn/types" + "github.com/kava-labs/kava/x/incentive/keeper" + "github.com/kava-labs/kava/x/incentive/types" + "github.com/stretchr/testify/suite" +) + +type QuerierTestSuite struct { + unitTester +} + +func TestQuerierTestSuite(t *testing.T) { + suite.Run(t, new(QuerierTestSuite)) +} + +func (suite *QuerierTestSuite) TestGetStakingAPR() { + communityTax := sdk.MustNewDecFromStr("0.90") + inflation := sdk.MustNewDecFromStr("0.75") + + bondedTokens := int64(120_000_000_000000) + liquidStakedTokens := int64(60_000_000_000000) + totalSupply := int64(289_138_414_286684) + + suite.keeper = suite.NewTestKeeper(&fakeParamSubspace{}). + WithDistrKeeper( + newFakeDistrKeeper().setCommunityTax(communityTax), + ). + WithMintKeeper( + newFakeMintKeeper(). + setMinter(minttypes.NewMinter(inflation, sdk.OneDec())), + ). + WithStakingKeeper( + newFakeStakingKeeper().addBondedTokens(bondedTokens), + ). + WithBankKeeper( + newFakeBankKeeper().setSupply(sdk.NewCoin(types.BondDenom, sdk.NewInt(totalSupply))), + ). + WithEarnKeeper( + newFakeEarnKeeper(). + addVault("bkava-asdf", earntypes.NewVaultShare("bkava-asdf", sdk.NewDec(liquidStakedTokens))), + ). + WithLiquidKeeper( + newFakeLiquidKeeper().addDerivative(suite.ctx, "bkava-asdf", sdk.NewInt(liquidStakedTokens)), + ). + WithPricefeedKeeper( + newFakePricefeedKeeper(). + setPrice( + pricefeedtypes.NewCurrentPrice( + "kava:usd:30", + sdk.MustNewDecFromStr("1.5"), + )), + ). + Build() + + // ~18% APR + expectedStakingAPY := inflation. + Mul(sdk.OneDec().Sub(communityTax)). + Quo(sdk.NewDec(bondedTokens).Quo(sdk.NewDec(totalSupply))) + + // Staking APR = (Inflation Rate * (1 - Community Tax)) / (Bonded Tokens / Circulating Supply) + aprWithoutIncentives, err := keeper.GetStakingAPR(suite.ctx, suite.keeper, types.Params{}) + suite.Require().NoError(err) + suite.Require().Equal( + expectedStakingAPY, + aprWithoutIncentives, + ) + + suite.T().Logf("Staking APR without incentives: %s", aprWithoutIncentives) + + params := types.Params{ + EarnRewardPeriods: types.MultiRewardPeriods{ + { + Active: true, + CollateralType: "bkava", + Start: suite.ctx.BlockTime().Add(-time.Hour), + End: suite.ctx.BlockTime().Add(time.Hour), + RewardsPerSecond: sdk.NewCoins( + sdk.NewCoin("ukava", sdk.NewInt(190258)), + ), + }, + }, + } + + aprWithIncentives, err := keeper.GetStakingAPR(suite.ctx, suite.keeper, params) + suite.Require().NoError(err) + // Approx 10% increase in APR from incentives + suite.Require().Equal(sdk.MustNewDecFromStr("0.280711113729177500"), aprWithIncentives) + + suite.Require().Truef( + aprWithIncentives.GT(aprWithoutIncentives), + "APR with incentives (%s) should be greater than APR without incentives (%s)", + ) +} diff --git a/x/incentive/keeper/unit_test.go b/x/incentive/keeper/unit_test.go index dda4a6b8..a7db3b47 100644 --- a/x/incentive/keeper/unit_test.go +++ b/x/incentive/keeper/unit_test.go @@ -8,8 +8,10 @@ import ( "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types" "github.com/stretchr/testify/suite" "github.com/tendermint/tendermint/libs/log" db "github.com/tendermint/tm-db" @@ -74,7 +76,11 @@ func (suite *unitTester) NewKeeper( ak types.AccountKeeper, stk types.StakingKeeper, swk types.SwapKeeper, svk types.SavingsKeeper, lqk types.LiquidKeeper, ek types.EarnKeeper, ) keeper.Keeper { - return keeper.NewKeeper(suite.cdc, suite.incentiveStoreKey, paramSubspace, bk, cdpk, hk, ak, stk, swk, svk, lqk, ek) + return keeper.NewKeeper( + suite.cdc, suite.incentiveStoreKey, paramSubspace, + bk, cdpk, hk, ak, stk, swk, svk, lqk, ek, + nil, nil, nil, + ) } func (suite *unitTester) storeGlobalBorrowIndexes(indexes types.MultiRewardIndexes) { @@ -133,6 +139,96 @@ func (suite *unitTester) storeEarnClaim(claim types.EarnClaim) { suite.keeper.SetEarnClaim(suite.ctx, claim) } +type TestKeeperBuilder struct { + cdc codec.Codec + key sdk.StoreKey + paramSubspace types.ParamSubspace + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper + cdpKeeper types.CdpKeeper + hardKeeper types.HardKeeper + stakingKeeper types.StakingKeeper + swapKeeper types.SwapKeeper + savingsKeeper types.SavingsKeeper + liquidKeeper types.LiquidKeeper + earnKeeper types.EarnKeeper + + // Keepers used for APY queries + mintKeeper types.MintKeeper + distrKeeper types.DistrKeeper + pricefeedKeeper types.PricefeedKeeper +} + +func (suite *unitTester) NewTestKeeper( + paramSubspace types.ParamSubspace, +) *TestKeeperBuilder { + if !paramSubspace.HasKeyTable() { + paramSubspace = paramSubspace.WithKeyTable(types.ParamKeyTable()) + } + + return &TestKeeperBuilder{ + cdc: suite.cdc, + key: suite.incentiveStoreKey, + paramSubspace: paramSubspace, + accountKeeper: nil, + bankKeeper: nil, + cdpKeeper: nil, + hardKeeper: nil, + stakingKeeper: nil, + swapKeeper: nil, + savingsKeeper: nil, + liquidKeeper: nil, + earnKeeper: nil, + mintKeeper: nil, + distrKeeper: nil, + pricefeedKeeper: nil, + } +} + +func (tk *TestKeeperBuilder) WithPricefeedKeeper(k types.PricefeedKeeper) *TestKeeperBuilder { + tk.pricefeedKeeper = k + return tk +} + +func (tk *TestKeeperBuilder) WithDistrKeeper(k types.DistrKeeper) *TestKeeperBuilder { + tk.distrKeeper = k + return tk +} + +func (tk *TestKeeperBuilder) WithBankKeeper(k types.BankKeeper) *TestKeeperBuilder { + tk.bankKeeper = k + return tk +} + +func (tk *TestKeeperBuilder) WithStakingKeeper(k types.StakingKeeper) *TestKeeperBuilder { + tk.stakingKeeper = k + return tk +} + +func (tk *TestKeeperBuilder) WithMintKeeper(k types.MintKeeper) *TestKeeperBuilder { + tk.mintKeeper = k + return tk +} + +func (tk *TestKeeperBuilder) WithEarnKeeper(k types.EarnKeeper) *TestKeeperBuilder { + tk.earnKeeper = k + return tk +} + +func (tk *TestKeeperBuilder) WithLiquidKeeper(k types.LiquidKeeper) *TestKeeperBuilder { + tk.liquidKeeper = k + return tk +} + +func (tk *TestKeeperBuilder) Build() keeper.Keeper { + return keeper.NewKeeper( + tk.cdc, tk.key, tk.paramSubspace, + tk.bankKeeper, tk.cdpKeeper, tk.hardKeeper, tk.accountKeeper, + tk.stakingKeeper, tk.swapKeeper, tk.savingsKeeper, tk.liquidKeeper, + tk.earnKeeper, tk.mintKeeper, tk.distrKeeper, tk.pricefeedKeeper, + ) +} + // fakeParamSubspace is a stub paramSpace to simplify keeper unit test setup. type fakeParamSubspace struct { params types.Params @@ -410,6 +506,15 @@ func (k *fakeEarnKeeper) GetVaultTotalShares( return vaultShares, found } +func (k *fakeEarnKeeper) GetVaultTotalValue(ctx sdk.Context, denom string) (sdk.Coin, error) { + vaultShares, found := k.vaultShares[denom] + if !found { + return sdk.NewCoin(denom, sdk.ZeroInt()), nil + } + + return sdk.NewCoin(denom, vaultShares.Amount.RoundInt()), nil +} + func (k *fakeEarnKeeper) GetVaultAccountShares( ctx sdk.Context, acc sdk.AccAddress, @@ -519,6 +624,112 @@ func (k *fakeLiquidKeeper) getRewardAmount( return amt.QuoRaw(10).MulRaw(duration) } +type fakeDistrKeeper struct { + communityTax sdk.Dec +} + +var _ types.DistrKeeper = newFakeDistrKeeper() + +func newFakeDistrKeeper() *fakeDistrKeeper { + return &fakeDistrKeeper{} +} + +func (k *fakeDistrKeeper) setCommunityTax(percent sdk.Dec) *fakeDistrKeeper { + k.communityTax = percent + return k +} + +func (k *fakeDistrKeeper) GetCommunityTax(ctx sdk.Context) (percent sdk.Dec) { + return k.communityTax +} + +type fakeMintKeeper struct { + minter minttypes.Minter +} + +var _ types.MintKeeper = newFakeMintKeeper() + +func newFakeMintKeeper() *fakeMintKeeper { + return &fakeMintKeeper{} +} + +func (k *fakeMintKeeper) setMinter(minter minttypes.Minter) *fakeMintKeeper { + k.minter = minter + return k +} + +func (k *fakeMintKeeper) GetMinter(ctx sdk.Context) (minter minttypes.Minter) { + return k.minter +} + +type fakePricefeedKeeper struct { + prices map[string]pricefeedtypes.CurrentPrice +} + +var _ types.PricefeedKeeper = newFakePricefeedKeeper() + +func newFakePricefeedKeeper() *fakePricefeedKeeper { + return &fakePricefeedKeeper{ + prices: map[string]pricefeedtypes.CurrentPrice{}, + } +} + +func (k *fakePricefeedKeeper) setPrice(price pricefeedtypes.CurrentPrice) *fakePricefeedKeeper { + k.prices[price.MarketID] = price + return k +} + +func (k *fakePricefeedKeeper) GetCurrentPrice(ctx sdk.Context, marketID string) (pricefeedtypes.CurrentPrice, error) { + price, found := k.prices[marketID] + if !found { + return pricefeedtypes.CurrentPrice{}, fmt.Errorf("price not found for market %s", marketID) + } + + return price, nil +} + +type fakeBankKeeper struct { + supply map[string]sdk.Int +} + +var _ types.BankKeeper = newFakeBankKeeper() + +func newFakeBankKeeper() *fakeBankKeeper { + return &fakeBankKeeper{ + supply: map[string]sdk.Int{}, + } +} + +func (k *fakeBankKeeper) setSupply(coins ...sdk.Coin) *fakeBankKeeper { + for _, coin := range coins { + k.supply[coin.Denom] = coin.Amount + } + + return k +} + +func (k *fakeBankKeeper) SendCoinsFromModuleToAccount( + ctx sdk.Context, + senderModule string, + recipientAddr sdk.AccAddress, + amt sdk.Coins, +) error { + panic("not implemented") +} + +func (k *fakeBankKeeper) GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins { + panic("not implemented") +} + +func (k *fakeBankKeeper) GetSupply(ctx sdk.Context, denom string) sdk.Coin { + supply, found := k.supply[denom] + if !found { + return sdk.NewCoin(denom, sdk.ZeroInt()) + } + + return sdk.NewCoin(denom, supply) +} + // Assorted Testing Data // note: amino panics when encoding times ≥ the start of year 10000. diff --git a/x/incentive/types/expected_keepers.go b/x/incentive/types/expected_keepers.go index 0e2cb629..d24538b7 100644 --- a/x/incentive/types/expected_keepers.go +++ b/x/incentive/types/expected_keepers.go @@ -3,11 +3,13 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" cdptypes "github.com/kava-labs/kava/x/cdp/types" earntypes "github.com/kava-labs/kava/x/earn/types" hardtypes "github.com/kava-labs/kava/x/hard/types" + pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types" savingstypes "github.com/kava-labs/kava/x/savings/types" ) @@ -23,6 +25,7 @@ type ParamSubspace interface { type BankKeeper interface { SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + GetSupply(ctx sdk.Context, denom string) sdk.Coin } // StakingKeeper defines the expected staking keeper for module accounts @@ -67,6 +70,7 @@ type SavingsKeeper interface { // EarnKeeper defines the required methods needed by this modules keeper type EarnKeeper interface { GetVaultTotalShares(ctx sdk.Context, denom string) (shares earntypes.VaultShare, found bool) + GetVaultTotalValue(ctx sdk.Context, denom string) (sdk.Coin, error) GetVaultAccountShares(ctx sdk.Context, acc sdk.AccAddress) (shares earntypes.VaultShares, found bool) IterateVaultRecords(ctx sdk.Context, cb func(record earntypes.VaultRecord) (stop bool)) } @@ -90,6 +94,21 @@ type AccountKeeper interface { GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI } +// MintKeeper defines the required methods needed by this modules keeper +type MintKeeper interface { + GetMinter(ctx sdk.Context) (minter minttypes.Minter) +} + +// DistrKeeper defines the required methods needed by this modules keeper +type DistrKeeper interface { + GetCommunityTax(ctx sdk.Context) (percent sdk.Dec) +} + +// PricefeedKeeper defines the required methods needed by this modules keeper +type PricefeedKeeper interface { + GetCurrentPrice(ctx sdk.Context, marketID string) (pricefeedtypes.CurrentPrice, error) +} + // CDPHooks event hooks for other keepers to run code in response to CDP modifications type CDPHooks interface { AfterCDPCreated(ctx sdk.Context, cdp cdptypes.CDP) diff --git a/x/incentive/types/querier.go b/x/incentive/types/querier.go index 6cbffa63..808a70ad 100644 --- a/x/incentive/types/querier.go +++ b/x/incentive/types/querier.go @@ -14,6 +14,7 @@ const ( QueryGetEarnRewards = "earn-rewards" QueryGetRewardFactors = "reward-factors" QueryGetParams = "parameters" + QueryGetAPYs = "apys" RestClaimCollateralType = "collateral_type" RestClaimOwner = "owner" @@ -64,3 +65,32 @@ func NewQueryGetRewardFactorsResponse(usdxMintingFactors RewardIndexes, supplyFa EarnRewardFactors: earnFactors, } } + +// APY contains the APY for a given collateral type +type APY struct { + CollateralType string `json:"collateral_type" yaml:"collateral_type"` + APY sdk.Dec `json:"apy" yaml:"apy"` +} + +// NewAPY returns a new instance of APY +func NewAPY(collateralType string, apy sdk.Dec) APY { + return APY{ + CollateralType: collateralType, + APY: apy, + } +} + +// APYs is a slice of APY +type APYs []APY + +// QueryGetAPYsResponse holds the response to a APY query +type QueryGetAPYsResponse struct { + Earn []APY `json:"earn" yaml:"earn"` +} + +// NewQueryGetAPYsResponse returns a new instance of QueryGetAPYsResponse +func NewQueryGetAPYsResponse(earn []APY) QueryGetAPYsResponse { + return QueryGetAPYsResponse{ + Earn: earn, + } +}