From d798c7f5ba602769839f2759cf1cfc51e02f704e Mon Sep 17 00:00:00 2001 From: Robert Pirtle Date: Tue, 24 Oct 2023 12:24:21 -0700 Subject: [PATCH] feat(community): add AnnualizedRewards grpc query (#1751) * add annualized_reward query proto * use sdkmath.LegacyDec to match RPS param... * add AnnualizedRewards grpc query * add changelog entry * simplify calculation & expand test cases (cherry picked from commit 0efe7f2281d63c37ff08cbe4edc5d9ee9493d4bf) # Conflicts: # CHANGELOG.md # client/docs/swagger-ui/swagger.yaml # x/community/keeper/staking.go # x/community/types/expected_keepers.go # x/community/types/query.pb.go --- CHANGELOG.md | 18 ++ client/docs/swagger-ui/swagger.yaml | 350 +++++++++++++++++++++ docs/core/proto-docs.md | 28 ++ proto/kava/community/v1beta1/query.proto | 20 ++ x/community/keeper/grpc_query.go | 35 +++ x/community/keeper/grpc_query_test.go | 131 +++++++- x/community/keeper/rewards.go | 27 ++ x/community/keeper/rewards_test.go | 189 ++++++++++++ x/community/keeper/staking.go | 98 ++++++ x/community/testutil/main.go | 3 +- x/community/types/expected_keepers.go | 32 ++ x/community/types/query.pb.go | 372 +++++++++++++++++++++++ x/community/types/query.pb.gw.go | 65 ++++ 13 files changed, 1364 insertions(+), 4 deletions(-) create mode 100644 x/community/keeper/rewards.go create mode 100644 x/community/keeper/rewards_test.go create mode 100644 x/community/keeper/staking.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 04e63fe399..f114b2dfd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,13 @@ Ref: https://keepachangelog.com/en/1.0.0/ ### Features - (metrics) [#1668] Adds non-state breaking x/metrics module for custom telemetry. - (metrics) [#1669] Add performance timing metrics to all Begin/EndBlockers +<<<<<<< HEAD +======= +- (community) [#1704] Add module params +- (community) [#1706] Add disable inflation upgrade +- (community) [#1745] Enable params update via governance with `MsgUpdateParams` +- (community) [#1751] Add `AnnualizedRewards` query endpoint +>>>>>>> 0efe7f22 (feat(community): add AnnualizedRewards grpc query (#1751)) ## [v0.24.0](https://github.com/Kava-Labs/kava/releases/tag/v0.24.0) @@ -277,6 +284,17 @@ the [changelog](https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/CHANGELOG.md). - [#257](https://github.com/Kava-Labs/kava/pulls/257) Include scripts to run large-scale simulations remotely using aws-batch +<<<<<<< HEAD +======= +[#1752]: https://github.com/Kava-Labs/kava/pull/1752 +[#1751]: https://github.com/Kava-Labs/kava/pull/1751 +[#1745]: https://github.com/Kava-Labs/kava/pull/1745 +[#1729]: https://github.com/Kava-Labs/kava/pull/1729 +[#1707]: https://github.com/Kava-Labs/kava/pull/1707 +[#1706]: https://github.com/Kava-Labs/kava/pull/1706 +[#1704]: https://github.com/Kava-Labs/kava/pull/1704 +[#1668]: https://github.com/Kava-Labs/kava/pull/1668 +>>>>>>> 0efe7f22 (feat(community): add AnnualizedRewards grpc query (#1751)) [#1669]: https://github.com/Kava-Labs/kava/pull/1669 [#1668]: https://github.com/Kava-Labs/kava/pull/1668 [#1624]: https://github.com/Kava-Labs/kava/pull/1624 diff --git a/client/docs/swagger-ui/swagger.yaml b/client/docs/swagger-ui/swagger.yaml index 2c98d4a6c4..4cf6686d31 100644 --- a/client/docs/swagger-ui/swagger.yaml +++ b/client/docs/swagger-ui/swagger.yaml @@ -12991,6 +12991,232 @@ paths: format: byte tags: - Savings +<<<<<<< HEAD +======= + /kava/community/v1beta1/annualized_rewards: + get: + summary: >- + AnnualizedRewards calculates and returns the current annualized reward + percentages, + + like staking rewards, for the chain. + operationId: CommunityAnnualizedRewards + responses: + '200': + description: A successful response. + schema: + type: object + properties: + staking_rewards: + type: string + title: >- + staking_rewards is the calculated annualized staking rewards + percentage rate + description: >- + QueryAnnualizedRewardsResponse defines the response type for + querying the annualized rewards. + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + value: + type: string + format: byte + tags: + - Community + /kava/community/v1beta1/balance: + get: + summary: Balance queries the balance of all coins of x/community module. + operationId: CommunityBalance + responses: + '200': + description: A successful response. + schema: + type: object + properties: + coins: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + Coin defines a token with a denomination and an amount. + + + NOTE: The amount field is an Int which implements the custom + method + + signatures required by gogoproto. + description: >- + QueryBalanceResponse defines the response type for querying + x/community balance. + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + value: + type: string + format: byte + tags: + - Community + /kava/community/v1beta1/params: + get: + summary: Params queires the module params. + operationId: CommunityParams + responses: + '200': + description: A successful response. + schema: + type: object + properties: + params: + title: params represents the community module parameters + type: object + properties: + upgrade_time_disable_inflation: + type: string + format: date-time + description: >- + upgrade_time_disable_inflation is the time at which to + disable mint and kavadist module inflation. + + If set to 0, inflation will be disabled from block 1. + staking_rewards_per_second: + type: string + title: >- + staking_rewards_per_second is the amount paid out to + delegators each block from the community account + upgrade_time_set_staking_rewards_per_second: + type: string + title: >- + upgrade_time_set_staking_rewards_per_second is the initial + staking_rewards_per_second to set + + and use when the disable inflation time is reached + description: Params defines the parameters of the community module. + description: >- + QueryParamsResponse defines the response type for querying + x/community params. + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + value: + type: string + format: byte + tags: + - Community + /kava/community/v1beta1/total_balance: + get: + summary: |- + TotalBalance queries the balance of all coins, including x/distribution, + x/community, and supplied balances. + operationId: CommunityTotalBalance + responses: + '200': + description: A successful response. + schema: + type: object + properties: + pool: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: >- + DecCoin defines a token with a denomination and a decimal + amount. + + + NOTE: The amount field is an Dec which implements the custom + method + + signatures required by gogoproto. + description: pool defines community pool's coins. + description: >- + QueryTotalBalanceResponse defines the response type for querying + total + + community pool balance. This matches the x/distribution + CommunityPool query response. + default: + description: An unexpected error response. + schema: + type: object + properties: + error: + type: string + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + properties: + type_url: + type: string + value: + type: string + format: byte + tags: + - Community +>>>>>>> 0efe7f22 (feat(community): add AnnualizedRewards grpc query (#1751)) /cosmos/auth/v1beta1/accounts: get: summary: Accounts returns all the existing accounts @@ -56830,6 +57056,130 @@ definitions: description: >- TotalSupplyResponse defines the response type for the Query/TotalSupply method. +<<<<<<< HEAD +======= + cosmos.base.v1beta1.DecCoin: + type: object + properties: + denom: + type: string + amount: + type: string + description: |- + DecCoin defines a token with a denomination and a decimal amount. + + NOTE: The amount field is an Dec which implements the custom method + signatures required by gogoproto. + kava.community.v1beta1.Params: + type: object + properties: + upgrade_time_disable_inflation: + type: string + format: date-time + description: >- + upgrade_time_disable_inflation is the time at which to disable mint + and kavadist module inflation. + + If set to 0, inflation will be disabled from block 1. + staking_rewards_per_second: + type: string + title: >- + staking_rewards_per_second is the amount paid out to delegators each + block from the community account + upgrade_time_set_staking_rewards_per_second: + type: string + title: >- + upgrade_time_set_staking_rewards_per_second is the initial + staking_rewards_per_second to set + + and use when the disable inflation time is reached + description: Params defines the parameters of the community module. + kava.community.v1beta1.QueryAnnualizedRewardsResponse: + type: object + properties: + staking_rewards: + type: string + title: >- + staking_rewards is the calculated annualized staking rewards + percentage rate + description: >- + QueryAnnualizedRewardsResponse defines the response type for querying the + annualized rewards. + kava.community.v1beta1.QueryBalanceResponse: + type: object + properties: + coins: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: |- + Coin defines a token with a denomination and an amount. + + NOTE: The amount field is an Int which implements the custom method + signatures required by gogoproto. + description: >- + QueryBalanceResponse defines the response type for querying x/community + balance. + kava.community.v1beta1.QueryParamsResponse: + type: object + properties: + params: + title: params represents the community module parameters + type: object + properties: + upgrade_time_disable_inflation: + type: string + format: date-time + description: >- + upgrade_time_disable_inflation is the time at which to disable + mint and kavadist module inflation. + + If set to 0, inflation will be disabled from block 1. + staking_rewards_per_second: + type: string + title: >- + staking_rewards_per_second is the amount paid out to delegators + each block from the community account + upgrade_time_set_staking_rewards_per_second: + type: string + title: >- + upgrade_time_set_staking_rewards_per_second is the initial + staking_rewards_per_second to set + + and use when the disable inflation time is reached + description: Params defines the parameters of the community module. + description: >- + QueryParamsResponse defines the response type for querying x/community + params. + kava.community.v1beta1.QueryTotalBalanceResponse: + type: object + properties: + pool: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: |- + DecCoin defines a token with a denomination and a decimal amount. + + NOTE: The amount field is an Dec which implements the custom method + signatures required by gogoproto. + description: pool defines community pool's coins. + description: >- + QueryTotalBalanceResponse defines the response type for querying total + + community pool balance. This matches the x/distribution CommunityPool + query response. +>>>>>>> 0efe7f22 (feat(community): add AnnualizedRewards grpc query (#1751)) cosmos.auth.v1beta1.AddressBytesToStringResponse: type: object properties: diff --git a/docs/core/proto-docs.md b/docs/core/proto-docs.md index 3eca9d5945..b0fe3771d6 100644 --- a/docs/core/proto-docs.md +++ b/docs/core/proto-docs.md @@ -188,6 +188,8 @@ - [CommunityPoolLendWithdrawProposal](#kava.community.v1beta1.CommunityPoolLendWithdrawProposal) - [kava/community/v1beta1/query.proto](#kava/community/v1beta1/query.proto) + - [QueryAnnualizedRewardsRequest](#kava.community.v1beta1.QueryAnnualizedRewardsRequest) + - [QueryAnnualizedRewardsResponse](#kava.community.v1beta1.QueryAnnualizedRewardsResponse) - [QueryBalanceRequest](#kava.community.v1beta1.QueryBalanceRequest) - [QueryBalanceResponse](#kava.community.v1beta1.QueryBalanceResponse) - [QueryTotalBalanceRequest](#kava.community.v1beta1.QueryTotalBalanceRequest) @@ -2977,6 +2979,31 @@ CommunityPoolLendWithdrawProposal withdraws a lend position back to the communit + + +### QueryAnnualizedRewardsRequest +QueryAnnualizedRewardsRequest defines the request type for querying the annualized rewards. + + + + + + + + +### QueryAnnualizedRewardsResponse +QueryAnnualizedRewardsResponse defines the response type for querying the annualized rewards. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `staking_rewards` | [string](#string) | | staking_rewards is the calculated annualized staking rewards percentage rate | + + + + + + ### QueryBalanceRequest @@ -3043,6 +3070,7 @@ Query defines the gRPC querier service for x/community. | ----------- | ------------ | ------------- | ------------| ------- | -------- | | `Balance` | [QueryBalanceRequest](#kava.community.v1beta1.QueryBalanceRequest) | [QueryBalanceResponse](#kava.community.v1beta1.QueryBalanceResponse) | Balance queries the balance of all coins of x/community module. | GET|/kava/community/v1beta1/balance| | `TotalBalance` | [QueryTotalBalanceRequest](#kava.community.v1beta1.QueryTotalBalanceRequest) | [QueryTotalBalanceResponse](#kava.community.v1beta1.QueryTotalBalanceResponse) | TotalBalance queries the balance of all coins, including x/distribution, x/community, and supplied balances. | GET|/kava/community/v1beta1/total_balance| +| `AnnualizedRewards` | [QueryAnnualizedRewardsRequest](#kava.community.v1beta1.QueryAnnualizedRewardsRequest) | [QueryAnnualizedRewardsResponse](#kava.community.v1beta1.QueryAnnualizedRewardsResponse) | AnnualizedRewards calculates and returns the current annualized reward percentages, like staking rewards, for the chain. | GET|/kava/community/v1beta1/annualized_rewards| diff --git a/proto/kava/community/v1beta1/query.proto b/proto/kava/community/v1beta1/query.proto index 53feab6bdb..cd066d6869 100644 --- a/proto/kava/community/v1beta1/query.proto +++ b/proto/kava/community/v1beta1/query.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package kava.community.v1beta1; import "cosmos/base/v1beta1/coin.proto"; +import "cosmos_proto/cosmos.proto"; import "gogoproto/gogo.proto"; import "google/api/annotations.proto"; @@ -19,6 +20,12 @@ service Query { rpc TotalBalance(QueryTotalBalanceRequest) returns (QueryTotalBalanceResponse) { option (google.api.http).get = "/kava/community/v1beta1/total_balance"; } + + // AnnualizedRewards calculates and returns the current annualized reward percentages, + // like staking rewards, for the chain. + rpc AnnualizedRewards(QueryAnnualizedRewardsRequest) returns (QueryAnnualizedRewardsResponse) { + option (google.api.http).get = "/kava/community/v1beta1/annualized_rewards"; + } } // QueryBalanceRequest defines the request type for querying x/community balance. @@ -44,3 +51,16 @@ message QueryTotalBalanceResponse { (gogoproto.nullable) = false ]; } + +// QueryAnnualizedRewardsRequest defines the request type for querying the annualized rewards. +message QueryAnnualizedRewardsRequest {} + +// QueryAnnualizedRewardsResponse defines the response type for querying the annualized rewards. +message QueryAnnualizedRewardsResponse { + // staking_rewards is the calculated annualized staking rewards percentage rate + string staking_rewards = 1 [ + (cosmos_proto.scalar) = "cosmos.Dec", + (gogoproto.customtype) = "cosmossdk.io/math.LegacyDec", + (gogoproto.nullable) = false + ]; +} diff --git a/x/community/keeper/grpc_query.go b/x/community/keeper/grpc_query.go index 205007942b..b9a49a3e9e 100644 --- a/x/community/keeper/grpc_query.go +++ b/x/community/keeper/grpc_query.go @@ -3,6 +3,7 @@ package keeper import ( "context" + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/kava-labs/kava/x/community/types" ) @@ -45,3 +46,37 @@ func (s queryServer) TotalBalance( Pool: totalBalance, }, nil } + +// AnnualizedRewards calculates the annualized rewards for the chain. +func (s queryServer) AnnualizedRewards( + c context.Context, + req *types.QueryAnnualizedRewardsRequest, +) (*types.QueryAnnualizedRewardsResponse, error) { + ctx := sdk.UnwrapSDKContext(c) + + // staking rewards come from one of two sources depending on if inflation is enabled or not. + // at any given time, only one source will contribute to the staking rewards. the other will be zero. + // this method adds both sources together so it is accurate in both cases. + + params := s.keeper.mustGetParams(ctx) + bondDenom := s.keeper.stakingKeeper.BondDenom(ctx) + + totalSupply := s.keeper.bankKeeper.GetSupply(ctx, bondDenom).Amount + totalBonded := s.keeper.stakingKeeper.TotalBondedTokens(ctx) + rewardsPerSecond := params.StakingRewardsPerSecond + // need to convert these from sdk.Dec to sdkmath.LegacyDec + inflationRate := convertDecToLegacyDec(s.keeper.mintKeeper.GetMinter(ctx).Inflation) + communityTax := convertDecToLegacyDec(s.keeper.distrKeeper.GetCommunityTax(ctx)) + + return &types.QueryAnnualizedRewardsResponse{ + StakingRewards: CalculateStakingAnnualPercentage(totalSupply, totalBonded, inflationRate, communityTax, rewardsPerSecond), + }, nil +} + +// convertDecToLegacyDec is a helper method for converting between new and old Dec types +// current version of cosmos-sdk in this repo uses sdk.Dec +// this module uses sdkmath.LegacyDec in its parameters +// TODO: remove me after upgrade to cosmos-sdk v50 (LegacyDec is everywhere) +func convertDecToLegacyDec(in sdk.Dec) sdkmath.LegacyDec { + return sdkmath.LegacyNewDecFromBigIntWithPrec(in.BigInt(), sdk.Precision) +} diff --git a/x/community/keeper/grpc_query_test.go b/x/community/keeper/grpc_query_test.go index a40b0e76a5..18d0c64bbd 100644 --- a/x/community/keeper/grpc_query_test.go +++ b/x/community/keeper/grpc_query_test.go @@ -9,18 +9,20 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/suite" + "github.com/kava-labs/kava/app" "github.com/kava-labs/kava/x/community/keeper" + "github.com/kava-labs/kava/x/community/testutil" "github.com/kava-labs/kava/x/community/types" ) type grpcQueryTestSuite struct { - KeeperTestSuite + testutil.Suite queryClient types.QueryClient } func (suite *grpcQueryTestSuite) SetupTest() { - suite.KeeperTestSuite.SetupTest() + suite.Suite.SetupTest() queryHelper := baseapp.NewQueryServerTestHelper(suite.Ctx, suite.App.InterfaceRegistry()) types.RegisterQueryServer(queryHelper, keeper.NewQueryServerImpl(suite.Keeper)) @@ -144,3 +146,128 @@ func (suite *grpcQueryTestSuite) TestGrpcQueryTotalBalance() { }) } } + +// NOTE: this test makes use of the fact that there is always an initial 1e6 bonded tokens +// To adjust the bonded ratio, it adjusts the total supply by minting tokens. +func (suite *grpcQueryTestSuite) TestGrpcQueryAnnualizedRewards() { + bondedTokens := sdkmath.NewInt(1e6) + testCases := []struct { + name string + bondedRatio sdk.Dec + inflation sdk.Dec + rewardsPerSec sdkmath.LegacyDec + communityTax sdk.Dec + expectedRate sdkmath.LegacyDec + }{ + { + name: "sanity check: no inflation, no rewards => 0%", + bondedRatio: sdk.MustNewDecFromStr("0.3456"), + inflation: sdk.ZeroDec(), + rewardsPerSec: sdkmath.LegacyZeroDec(), + expectedRate: sdkmath.LegacyZeroDec(), + }, + { + name: "inflation sanity check: 100% inflation, 100% bonded => 100%", + bondedRatio: sdk.OneDec(), + inflation: sdk.OneDec(), + rewardsPerSec: sdkmath.LegacyZeroDec(), + expectedRate: sdkmath.LegacyOneDec(), + }, + { + name: "inflation sanity check: 100% community tax => 0%", + bondedRatio: sdk.OneDec(), + inflation: sdk.OneDec(), + communityTax: sdk.OneDec(), + rewardsPerSec: sdkmath.LegacyZeroDec(), + expectedRate: sdkmath.LegacyZeroDec(), + }, + { + name: "rewards per second sanity check: (totalBonded/SecondsPerYear) rps => 100%", + bondedRatio: sdk.OneDec(), // bonded tokens are constant in this test. ratio has no affect. + inflation: sdk.ZeroDec(), + rewardsPerSec: sdkmath.LegacyNewDecFromInt(bondedTokens).QuoInt(sdkmath.NewInt(keeper.SecondsPerYear)), + // expect ~100% + expectedRate: sdkmath.LegacyMustNewDecFromStr("0.999999999999999984"), + }, + { + name: "inflation enabled: realistic example", + bondedRatio: sdk.MustNewDecFromStr("0.148"), + inflation: sdk.MustNewDecFromStr("0.595"), + communityTax: sdk.MustNewDecFromStr("0.9495"), + rewardsPerSec: sdkmath.LegacyZeroDec(), + // expect ~20.23% + expectedRate: sdkmath.LegacyMustNewDecFromStr("0.203023625910000000"), + }, + { + name: "inflation disabled: simple example", + bondedRatio: sdk.OneDec(), // bonded tokens are constant in this test. ratio has no affect. + inflation: sdk.ZeroDec(), + rewardsPerSec: sdkmath.LegacyMustNewDecFromStr("0.01"), + // 1e6 bonded tokens => seconds per year / bonded tokens = 31.536 + // expect 31.536% + expectedRate: sdkmath.LegacyMustNewDecFromStr("0.31536"), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + + // set inflation + mk := suite.App.GetMintKeeper() + minter := mk.GetMinter(suite.Ctx) + minter.Inflation = tc.inflation + mk.SetMinter(suite.Ctx, minter) + + // set community tax + communityTax := sdk.ZeroDec() + if !tc.communityTax.IsNil() { + communityTax = tc.communityTax + } + dk := suite.App.GetDistrKeeper() + distParams := dk.GetParams(suite.Ctx) + distParams.CommunityTax = communityTax + dk.SetParams(suite.Ctx, distParams) + + // set staking rewards per second + ck := suite.App.GetCommunityKeeper() + commParams, _ := ck.GetParams(suite.Ctx) + commParams.StakingRewardsPerSecond = tc.rewardsPerSec + ck.SetParams(suite.Ctx, commParams) + + // set bonded tokens + suite.adjustBondedRatio(tc.bondedRatio) + + // query for annualized rewards + res, err := suite.queryClient.AnnualizedRewards(suite.Ctx, &types.QueryAnnualizedRewardsRequest{}) + // verify results match expected + suite.Require().NoError(err) + suite.Equal(tc.expectedRate, res.StakingRewards) + }) + } +} + +// adjustBondRatio changes the ratio of bonded coins +// it leverages the fact that there is a constant number of bonded tokens +// and adjusts the total supply to make change the bonded ratio. +// returns the new total supply of the bond denom +func (suite *grpcQueryTestSuite) adjustBondedRatio(desiredRatio sdk.Dec) sdkmath.Int { + // from the InitGenesis validator + bondedTokens := sdkmath.NewInt(1e6) + bondDenom := suite.App.GetStakingKeeper().BondDenom(suite.Ctx) + + // first, burn all non-delegated coins (bonded ratio = 100%) + suite.App.DeleteGenesisValidatorCoins(suite.T(), suite.Ctx) + + if desiredRatio.Equal(sdk.OneDec()) { + return bondedTokens + } + + // mint new tokens to adjust the bond ratio + newTotalSupply := sdk.NewDecFromInt(bondedTokens).Quo(desiredRatio).TruncateInt() + coinsToMint := newTotalSupply.Sub(bondedTokens) + err := suite.App.FundAccount(suite.Ctx, app.RandomAddress(), sdk.NewCoins(sdk.NewCoin(bondDenom, coinsToMint))) + suite.Require().NoError(err) + + return newTotalSupply +} diff --git a/x/community/keeper/rewards.go b/x/community/keeper/rewards.go new file mode 100644 index 0000000000..1f9198a340 --- /dev/null +++ b/x/community/keeper/rewards.go @@ -0,0 +1,27 @@ +package keeper + +import ( + sdkmath "cosmossdk.io/math" +) + +const SecondsPerYear = 365 * 24 * 3600 + +// CalculateStakingAnnualPercentage returns the annualized staking reward rate. +// It assumes that staking comes from one of two sources depending on if inflation is enabled or not. +func CalculateStakingAnnualPercentage(totalSupply, totalBonded sdkmath.Int, inflationRate, communityTax, rewardsPerSecond sdkmath.LegacyDec) sdkmath.LegacyDec { + // no rewards are given if no tokens are bonded, in addition avoid division by zero + if totalBonded.IsZero() { + return sdkmath.LegacyZeroDec() + } + + // the percent of inflationRate * totalSupply tokens that are distributed to stakers + percentInflationDistributedToStakers := sdkmath.LegacyOneDec().Sub(communityTax) + + // the total amount of tokens distributed to stakers in a year + amountGivenPerYear := inflationRate. + MulInt(totalSupply).Mul(percentInflationDistributedToStakers). // portion provided by inflation via mint & distribution modules + Add(rewardsPerSecond.Mul(sdkmath.LegacyNewDec(SecondsPerYear))) // portion provided by community module + + // divide by total bonded tokens to get the percent return + return amountGivenPerYear.QuoInt(totalBonded) +} diff --git a/x/community/keeper/rewards_test.go b/x/community/keeper/rewards_test.go new file mode 100644 index 0000000000..8aed1f76a1 --- /dev/null +++ b/x/community/keeper/rewards_test.go @@ -0,0 +1,189 @@ +package keeper_test + +import ( + "math/big" + "testing" + + sdkmath "cosmossdk.io/math" + "github.com/stretchr/testify/require" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/community/keeper" +) + +func TestStakingRewardsCalculator(t *testing.T) { + hugeInflation := new(big.Int).Exp(big.NewInt(2), big.NewInt(205), nil) + hugeRewardsPerSec := new(big.Int).Exp(big.NewInt(2), big.NewInt(230), nil) + + testCases := []struct { + name string + totalSupply sdkmath.Int + totalBonded sdkmath.Int + inflation sdkmath.LegacyDec + communityTax sdkmath.LegacyDec + perSecReward sdkmath.LegacyDec + expectedRate sdkmath.LegacyDec + }{ + { + name: "no inflation, no rewards per sec -> 0%", + totalSupply: sdkmath.ZeroInt(), + totalBonded: sdkmath.ZeroInt(), + inflation: sdkmath.LegacyZeroDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyZeroDec(), + expectedRate: sdkmath.LegacyZeroDec(), + }, + // + // + // inflation-only + // + // + { + name: "inflation only: no bonded tokens -> 0%", + totalSupply: sdk.NewInt(42), + totalBonded: sdkmath.ZeroInt(), + inflation: sdkmath.LegacyOneDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyZeroDec(), + expectedRate: sdkmath.LegacyZeroDec(), + }, + { + name: "inflation only: 0% inflation -> 0%", + totalSupply: sdk.NewInt(123), + totalBonded: sdkmath.NewInt(45), + inflation: sdkmath.LegacyZeroDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyZeroDec(), + expectedRate: sdkmath.LegacyZeroDec(), + }, + { + name: "inflation only: 100% bonded w/ 100% inflation -> 100%", + totalSupply: sdk.NewInt(42), + totalBonded: sdk.NewInt(42), + inflation: sdkmath.LegacyOneDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyZeroDec(), + expectedRate: sdkmath.LegacyOneDec(), + }, + { + name: "inflation only: 100% community tax -> 0%", + totalSupply: sdk.NewInt(123), + totalBonded: sdkmath.NewInt(45), + inflation: sdkmath.LegacyMustNewDecFromStr("0.853"), + communityTax: sdkmath.LegacyOneDec(), + perSecReward: sdkmath.LegacyZeroDec(), + expectedRate: sdkmath.LegacyZeroDec(), + }, + { + name: "inflation only: Oct 2023 case", + totalSupply: sdk.NewInt(857570000e6), + totalBonded: sdk.NewInt(127680000e6), + inflation: sdkmath.LegacyMustNewDecFromStr("0.595"), + communityTax: sdkmath.LegacyMustNewDecFromStr("0.9495"), + perSecReward: sdkmath.LegacyZeroDec(), + // expect 20.18% staking reward + expectedRate: sdkmath.LegacyMustNewDecFromStr("0.201815746984649122"), // verified manually + }, + { + name: "inflation only: low inflation", + totalSupply: sdk.NewInt(857570000e6), + totalBonded: sdk.NewInt(127680000e6), + inflation: sdkmath.LegacyMustNewDecFromStr("0.0000000001"), + communityTax: sdkmath.LegacyMustNewDecFromStr("0.9495"), + perSecReward: sdkmath.LegacyZeroDec(), + expectedRate: sdkmath.LegacyMustNewDecFromStr("0.000000000033918612"), // verified manually, rounded would be 0.000000000033918613 + }, + { + name: "inflation only: absurdly high inflation", + totalSupply: sdk.NewInt(857570000e6), + totalBonded: sdk.NewInt(127680000e6), + inflation: sdkmath.LegacyNewDecFromBigInt(hugeInflation), // 2^205. a higher exponent than this overflows. + communityTax: sdkmath.LegacyMustNewDecFromStr("0.9495"), + perSecReward: sdkmath.LegacyZeroDec(), + // https://www.wolframalpha.com/input?i=%282%5E205%29+*+%281+-+0.9495%29+*+%28857570000e6+%2F127680000e6%29 + expectedRate: sdkmath.LegacyMustNewDecFromStr("17441635052648297161685283657196753398188161373334495592570113.113824561403508771"), // verified manually, would round up + }, + // + // + // rewards-only + // + // + { + name: "rps only: no bonded tokens -> 0%", + totalSupply: sdk.NewInt(42), + totalBonded: sdkmath.ZeroInt(), + inflation: sdkmath.LegacyZeroDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyMustNewDecFromStr("1234567.123456"), + expectedRate: sdkmath.LegacyZeroDec(), + }, + { + name: "rps only: rps = total bonded / seconds in year -> basically 100%", + totalSupply: sdk.NewInt(12345), + totalBonded: sdkmath.NewInt(1234), + inflation: sdkmath.LegacyZeroDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyNewDec(1234).Quo(sdkmath.LegacyNewDec(keeper.SecondsPerYear)), + expectedRate: sdkmath.LegacyMustNewDecFromStr("0.999999999999987228"), // <-- for 6-decimal token, this is negligible rounding + }, + { + name: "rps only: 10M kava / year rewards", + totalSupply: sdk.NewInt(870950000e6), + totalBonded: sdkmath.NewInt(130380000e6), + inflation: sdkmath.LegacyZeroDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyMustNewDecFromStr("317097.919837645865043125"), // 10 million kava per year + expectedRate: sdkmath.LegacyMustNewDecFromStr("0.076698880196349133"), // verified manually + }, + { + name: "rps only: 25M kava / year rewards", + totalSupply: sdk.NewInt(870950000e6), + totalBonded: sdkmath.NewInt(130380000e6), + inflation: sdkmath.LegacyZeroDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyMustNewDecFromStr("792744.799594114662607813"), // 25 million kava per year + expectedRate: sdkmath.LegacyMustNewDecFromStr("0.191747200490872833"), // verified manually + }, + { + name: "rps only: too much kava / year rewards", + totalSupply: sdk.NewInt(870950000e6), + totalBonded: sdkmath.NewInt(130380000e6), + inflation: sdkmath.LegacyZeroDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyNewDecFromBigInt(hugeRewardsPerSec), // 2^230. a higher exponent than this overflows. + // https://www.wolframalpha.com/input?i=%28%28365+*+24+*+3600%29+%2F+130380000e6%29+*+%282%5E230%29 + expectedRate: sdkmath.LegacyMustNewDecFromStr("417344440850566075319340506352140425426634017001007267992800590.431305795858260469"), // verified manually + }, + { + name: "rps only: low kava / year rewards", + totalSupply: sdk.NewInt(870950000e6), + totalBonded: sdkmath.NewInt(130380000e6), + inflation: sdkmath.LegacyZeroDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyMustNewDecFromStr("0.1"), + expectedRate: sdkmath.LegacyMustNewDecFromStr("0.000000024187758858"), // verified manually, rounded would be 0.000000024187758859 + }, + { + name: "rps only: 1 ukava / year rewards", + totalSupply: sdk.NewInt(870950000e6), + totalBonded: sdkmath.NewInt(130380000e6), + inflation: sdkmath.LegacyZeroDec(), + communityTax: sdkmath.LegacyZeroDec(), + perSecReward: sdkmath.LegacyMustNewDecFromStr("0.000000031709791984"), // 1 ukava per year + expectedRate: sdkmath.LegacyMustNewDecFromStr("0.000000000000007669"), // verified manually, rounded would be 0.000000000000007670 + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + rewardRate := keeper.CalculateStakingAnnualPercentage( + tc.totalSupply, + tc.totalBonded, + tc.inflation, + tc.communityTax, + tc.perSecReward) + require.Equal(t, tc.expectedRate, rewardRate) + }) + } +} diff --git a/x/community/keeper/staking.go b/x/community/keeper/staking.go new file mode 100644 index 0000000000..52101a1148 --- /dev/null +++ b/x/community/keeper/staking.go @@ -0,0 +1,98 @@ +package keeper + +import ( + "time" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/kava-labs/kava/x/community/types" +) + +const nanosecondsInOneSecond = int64(1000000000) + +// PayoutAccumulatedStakingRewards calculates and transfers taking rewards to the fee collector address +func (k Keeper) PayoutAccumulatedStakingRewards(ctx sdk.Context) { + // get module parameters which define the amount of rewards to payout per second + params := k.mustGetParams(ctx) + currentBlockTime := ctx.BlockTime() + state := k.GetStakingRewardsState(ctx) + + // we have un-initialized state -- set accumulation time and exit since there is nothing to do + if state.LastAccumulationTime.IsZero() { + state.LastAccumulationTime = currentBlockTime + + k.SetStakingRewardsState(ctx, state) + + return + } + + // get the denom for staking + stakingRewardDenom := k.stakingKeeper.BondDenom(ctx) + + // we fetch the community pool balance to ensure only accumulate rewards up to the current balance + communityPoolBalance := sdkmath.LegacyNewDecFromInt(k.bankKeeper.GetBalance(ctx, k.moduleAddress, stakingRewardDenom).Amount) + + // calculate staking reward payout capped to community pool balance + truncatedRewards, truncationError := calculateStakingRewards( + currentBlockTime, + state.LastAccumulationTime, + state.LastTruncationError, + params.StakingRewardsPerSecond, + communityPoolBalance, + ) + + // only payout if the truncated rewards are non-zero + if !truncatedRewards.IsZero() { + transferAmount := sdk.NewCoins(sdk.NewCoin(stakingRewardDenom, truncatedRewards)) + + if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleAccountName, authtypes.FeeCollectorName, transferAmount); err != nil { + // we check for a valid balance and rewards can never be negative so panic since this will only + // occur in cases where the chain is running in an invalid state + panic(err) + } + + // emit event with amount transferred + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeStakingRewardsPaid, + sdk.NewAttribute(types.AttributeKeyStakingRewardAmount, transferAmount.String()), + ), + ) + + } + + // update accumulation state + state.LastAccumulationTime = currentBlockTime + // if the community pool balance is zero, this also resets the truncation error + state.LastTruncationError = truncationError + + // save state + k.SetStakingRewardsState(ctx, state) +} + +// calculateStakingRewards takes the currentBlockTime, state of last accumulation, rewards per second, and the community pool balance +// in order to calculate the total payout since the last accumulation time. It returns the truncated payout amount and the truncation error. +func calculateStakingRewards(currentBlockTime, lastAccumulationTime time.Time, lastTruncationError, stakingRewardsPerSecond, communityPoolBalance sdkmath.LegacyDec) (sdkmath.Int, sdkmath.LegacyDec) { + // we get the duration since we last accumulated, then use nanoseconds for full precision available + durationSinceLastPayout := currentBlockTime.Sub(lastAccumulationTime) + nanosecondsSinceLastPayout := sdkmath.LegacyNewDec(durationSinceLastPayout.Nanoseconds()) + + // We multiply by nanoseconds first, then divide by conversion to avoid loss of precision. + // This multiplication is also tested against very large values so we are safe from overflow + // in normal operations. + accumulatedRewards := nanosecondsSinceLastPayout.Mul(stakingRewardsPerSecond).QuoInt64(nanosecondsInOneSecond) + // Ensure we add any error from previous truncations + accumulatedRewards = accumulatedRewards.Add(lastTruncationError) + + if communityPoolBalance.LT(accumulatedRewards) { + accumulatedRewards = communityPoolBalance + } + + // we truncate since we can only transfer whole units + truncatedRewards := accumulatedRewards.TruncateDec() + // the truncation error to carry over to the next accumulation + truncationError := accumulatedRewards.Sub(truncatedRewards) + + return truncatedRewards.TruncateInt(), truncationError +} diff --git a/x/community/testutil/main.go b/x/community/testutil/main.go index c9ec69fde4..b802e9dcdc 100644 --- a/x/community/testutil/main.go +++ b/x/community/testutil/main.go @@ -28,9 +28,8 @@ func (suite *Suite) SetupTest() { tApp := app.NewTestApp() ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()}) - tApp.InitializeFromGenesisStates() + suite.App = tApp.InitializeFromGenesisStates() - suite.App = tApp suite.Ctx = ctx suite.Keeper = tApp.GetCommunityKeeper() communityPoolAddress := tApp.GetAccountKeeper().GetModuleAddress(types.ModuleAccountName) diff --git a/x/community/types/expected_keepers.go b/x/community/types/expected_keepers.go index f642dfe0be..f0df0c0756 100644 --- a/x/community/types/expected_keepers.go +++ b/x/community/types/expected_keepers.go @@ -1,6 +1,7 @@ package types import ( + sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" ) @@ -17,6 +18,12 @@ type BankKeeper interface { SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error +<<<<<<< HEAD +======= + SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error + + GetSupply(ctx sdk.Context, denom string) sdk.Coin +>>>>>>> 0efe7f22 (feat(community): add AnnualizedRewards grpc query (#1751)) } // CdpKeeper defines the contract needed to be fulfilled for cdp dependencies. @@ -36,4 +43,29 @@ type DistributionKeeper interface { DistributeFromFeePool(ctx sdk.Context, amount sdk.Coins, receiveAddr sdk.AccAddress) error FundCommunityPool(ctx sdk.Context, amount sdk.Coins, sender sdk.AccAddress) error GetFeePoolCommunityCoins(ctx sdk.Context) sdk.DecCoins +<<<<<<< HEAD +======= + GetFeePool(ctx sdk.Context) distrtypes.FeePool + SetFeePool(ctx sdk.Context, feePool distrtypes.FeePool) + GetParams(ctx sdk.Context) distrtypes.Params + SetParams(ctx sdk.Context, params distrtypes.Params) + GetCommunityTax(ctx sdk.Context) sdk.Dec +} + +type MintKeeper interface { + GetParams(ctx sdk.Context) (params minttypes.Params) + SetParams(ctx sdk.Context, params minttypes.Params) + GetMinter(ctx sdk.Context) (minter minttypes.Minter) +} + +type KavadistKeeper interface { + GetParams(ctx sdk.Context) (params kavadisttypes.Params) + SetParams(ctx sdk.Context, params kavadisttypes.Params) +} + +// StakingKeeper expected interface for the staking keeper +type StakingKeeper interface { + BondDenom(ctx sdk.Context) string + TotalBondedTokens(ctx sdk.Context) sdkmath.Int +>>>>>>> 0efe7f22 (feat(community): add AnnualizedRewards grpc query (#1751)) } diff --git a/x/community/types/query.pb.go b/x/community/types/query.pb.go index 5da1aae9df..e06765e7e3 100644 --- a/x/community/types/query.pb.go +++ b/x/community/types/query.pb.go @@ -5,7 +5,9 @@ package types import ( context "context" + cosmossdk_io_math "cosmossdk.io/math" fmt "fmt" + _ "github.com/cosmos/cosmos-proto" github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" types "github.com/cosmos/cosmos-sdk/types" _ "github.com/gogo/protobuf/gogoproto" @@ -197,11 +199,89 @@ func (m *QueryTotalBalanceResponse) GetPool() github_com_cosmos_cosmos_sdk_types return nil } +// QueryAnnualizedRewardsRequest defines the request type for querying the annualized rewards. +type QueryAnnualizedRewardsRequest struct { +} + +func (m *QueryAnnualizedRewardsRequest) Reset() { *m = QueryAnnualizedRewardsRequest{} } +func (m *QueryAnnualizedRewardsRequest) String() string { return proto.CompactTextString(m) } +func (*QueryAnnualizedRewardsRequest) ProtoMessage() {} +func (*QueryAnnualizedRewardsRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_f236f06c43149273, []int{6} +} +func (m *QueryAnnualizedRewardsRequest) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryAnnualizedRewardsRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryAnnualizedRewardsRequest.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 *QueryAnnualizedRewardsRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryAnnualizedRewardsRequest.Merge(m, src) +} +func (m *QueryAnnualizedRewardsRequest) XXX_Size() int { + return m.Size() +} +func (m *QueryAnnualizedRewardsRequest) XXX_DiscardUnknown() { + xxx_messageInfo_QueryAnnualizedRewardsRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryAnnualizedRewardsRequest proto.InternalMessageInfo + +// QueryAnnualizedRewardsResponse defines the response type for querying the annualized rewards. +type QueryAnnualizedRewardsResponse struct { + // staking_rewards is the calculated annualized staking rewards percentage rate + StakingRewards cosmossdk_io_math.LegacyDec `protobuf:"bytes,1,opt,name=staking_rewards,json=stakingRewards,proto3,customtype=cosmossdk.io/math.LegacyDec" json:"staking_rewards"` +} + +func (m *QueryAnnualizedRewardsResponse) Reset() { *m = QueryAnnualizedRewardsResponse{} } +func (m *QueryAnnualizedRewardsResponse) String() string { return proto.CompactTextString(m) } +func (*QueryAnnualizedRewardsResponse) ProtoMessage() {} +func (*QueryAnnualizedRewardsResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_f236f06c43149273, []int{7} +} +func (m *QueryAnnualizedRewardsResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *QueryAnnualizedRewardsResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_QueryAnnualizedRewardsResponse.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 *QueryAnnualizedRewardsResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_QueryAnnualizedRewardsResponse.Merge(m, src) +} +func (m *QueryAnnualizedRewardsResponse) XXX_Size() int { + return m.Size() +} +func (m *QueryAnnualizedRewardsResponse) XXX_DiscardUnknown() { + xxx_messageInfo_QueryAnnualizedRewardsResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_QueryAnnualizedRewardsResponse proto.InternalMessageInfo + func init() { proto.RegisterType((*QueryBalanceRequest)(nil), "kava.community.v1beta1.QueryBalanceRequest") proto.RegisterType((*QueryBalanceResponse)(nil), "kava.community.v1beta1.QueryBalanceResponse") proto.RegisterType((*QueryTotalBalanceRequest)(nil), "kava.community.v1beta1.QueryTotalBalanceRequest") proto.RegisterType((*QueryTotalBalanceResponse)(nil), "kava.community.v1beta1.QueryTotalBalanceResponse") + proto.RegisterType((*QueryAnnualizedRewardsRequest)(nil), "kava.community.v1beta1.QueryAnnualizedRewardsRequest") + proto.RegisterType((*QueryAnnualizedRewardsResponse)(nil), "kava.community.v1beta1.QueryAnnualizedRewardsResponse") } func init() { @@ -209,6 +289,7 @@ func init() { } var fileDescriptor_f236f06c43149273 = []byte{ +<<<<<<< HEAD // 411 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x92, 0x4f, 0x8e, 0xd3, 0x30, 0x14, 0xc6, 0x93, 0x42, 0x41, 0x32, 0xac, 0x42, 0x41, 0x6d, 0x54, 0xa5, 0x10, 0x09, 0xb5, 0x52, @@ -236,6 +317,47 @@ var fileDescriptor_f236f06c43149273 = []byte{ 0x79, 0xd6, 0x66, 0xe7, 0x59, 0x67, 0x3b, 0xcf, 0xfa, 0x72, 0x38, 0x0b, 0x65, 0xd5, 0x4b, 0x68, 0x20, 0x0a, 0xd3, 0x1f, 0x07, 0xb6, 0x7a, 0x28, 0xc1, 0x13, 0xfd, 0x7f, 0x47, 0xd7, 0x01, 0x00, 0x00, 0xff, 0xff, 0x67, 0x9d, 0xae, 0xd2, 0x51, 0x03, 0x00, 0x00, +======= + // 606 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x94, 0x31, 0x6f, 0xd3, 0x40, + 0x14, 0xc7, 0x63, 0x68, 0x8b, 0xb8, 0x22, 0x10, 0x47, 0x40, 0x8d, 0x29, 0x4e, 0x31, 0x82, 0x46, + 0x6d, 0x63, 0x37, 0xa9, 0x60, 0x62, 0x21, 0x84, 0x8d, 0x01, 0x0c, 0x53, 0x97, 0xe8, 0xec, 0x9c, + 0x5c, 0x2b, 0x8e, 0xcf, 0xcd, 0x5d, 0x0a, 0x41, 0x2c, 0x74, 0x63, 0x40, 0x42, 0xe2, 0x1b, 0x30, + 0x32, 0x23, 0x3e, 0x43, 0xc7, 0x0a, 0x16, 0xc4, 0x50, 0x50, 0xc2, 0x07, 0x41, 0x77, 0xf7, 0x12, + 0x25, 0x34, 0x8e, 0xd2, 0x29, 0xf1, 0xbb, 0xf7, 0x7f, 0xff, 0xdf, 0xbd, 0xf7, 0x6c, 0x64, 0xb7, + 0xc8, 0x01, 0x71, 0x03, 0xd6, 0x6e, 0x77, 0x93, 0x48, 0xf4, 0xdc, 0x83, 0x8a, 0x4f, 0x05, 0xa9, + 0xb8, 0xfb, 0x5d, 0xda, 0xe9, 0x39, 0x69, 0x87, 0x09, 0x86, 0x6f, 0xc8, 0x1c, 0x67, 0x94, 0xe3, + 0x40, 0x8e, 0x69, 0x05, 0x8c, 0xb7, 0x19, 0x77, 0x7d, 0xc2, 0xe9, 0x48, 0x18, 0xb0, 0x28, 0xd1, + 0x3a, 0xb3, 0xa0, 0xcf, 0x1b, 0xea, 0xc9, 0xd5, 0x0f, 0x70, 0x94, 0x0f, 0x59, 0xc8, 0x74, 0x5c, + 0xfe, 0x83, 0xe8, 0x6a, 0xc8, 0x58, 0x18, 0x53, 0x97, 0xa4, 0x91, 0x4b, 0x92, 0x84, 0x09, 0x22, + 0x22, 0x96, 0x0c, 0x35, 0x77, 0x32, 0x50, 0x53, 0xd2, 0x21, 0x6d, 0x48, 0xb2, 0xf3, 0x08, 0x3f, + 0x97, 0xe8, 0xcf, 0x54, 0xd0, 0xa3, 0xfb, 0x5d, 0xca, 0x85, 0xfd, 0x02, 0x5d, 0x9b, 0x88, 0xf2, + 0x94, 0x25, 0x9c, 0xe2, 0x87, 0x68, 0x49, 0x8b, 0x57, 0x8c, 0x35, 0xa3, 0xb4, 0x5c, 0xb5, 0x9c, + 0xe9, 0x37, 0x75, 0xb4, 0xae, 0xb6, 0x70, 0x74, 0x52, 0xcc, 0x79, 0xa0, 0xb1, 0xaf, 0x43, 0xd1, + 0x1a, 0x89, 0x49, 0x12, 0xd0, 0xa1, 0x57, 0x0f, 0xe5, 0x27, 0xc3, 0x60, 0x46, 0xd0, 0xa2, 0xec, + 0x8d, 0xf4, 0x3a, 0x5f, 0x5a, 0xae, 0x16, 0x1c, 0x68, 0x88, 0xec, 0xde, 0xc8, 0xe8, 0x31, 0x8b, + 0x92, 0xda, 0xb6, 0xb4, 0xf9, 0xf2, 0xbb, 0x58, 0x0a, 0x23, 0xb1, 0xd7, 0xf5, 0x25, 0x0f, 0x74, + 0x0f, 0x7e, 0xca, 0xbc, 0xd9, 0x72, 0x45, 0x2f, 0xa5, 0x5c, 0x09, 0xb8, 0xa7, 0x2b, 0xdb, 0x26, + 0x5a, 0x51, 0xd6, 0x2f, 0x99, 0x20, 0xf1, 0x7f, 0x58, 0x87, 0x06, 0x2a, 0x4c, 0x39, 0x04, 0x38, + 0x8a, 0x16, 0x52, 0xc6, 0x62, 0x60, 0x5b, 0x9d, 0xca, 0x56, 0xa7, 0x81, 0xc2, 0xdb, 0x01, 0xbc, + 0xcd, 0x39, 0xf0, 0x40, 0xc3, 0x3d, 0x55, 0xde, 0x2e, 0xa2, 0x5b, 0x8a, 0xe1, 0x51, 0x92, 0x74, + 0x49, 0x1c, 0xbd, 0xa1, 0x4d, 0x8f, 0xbe, 0x22, 0x9d, 0xe6, 0x68, 0x50, 0x6f, 0x91, 0x95, 0x95, + 0x00, 0xa4, 0xbb, 0xe8, 0x0a, 0x17, 0xa4, 0x15, 0x25, 0x61, 0xa3, 0xa3, 0x8f, 0xd4, 0xf0, 0x2e, + 0xd6, 0x2a, 0x12, 0xeb, 0xd7, 0x49, 0xf1, 0xa6, 0x86, 0xe0, 0xcd, 0x96, 0x13, 0x31, 0xb7, 0x4d, + 0xc4, 0x9e, 0xf3, 0x94, 0x86, 0x24, 0xe8, 0xd5, 0x69, 0xf0, 0xfd, 0x6b, 0x19, 0xc1, 0xd5, 0xea, + 0x34, 0xf0, 0x2e, 0x43, 0x25, 0xf0, 0xa8, 0xbe, 0x5b, 0x44, 0x8b, 0xca, 0x1e, 0xbf, 0x37, 0xd0, + 0x92, 0x1e, 0x3a, 0xde, 0xc8, 0x5a, 0x8a, 0xd3, 0x7b, 0x66, 0x6e, 0xce, 0x95, 0xab, 0x6f, 0x62, + 0xdf, 0x3b, 0xfc, 0xf1, 0xf7, 0xd3, 0xb9, 0x35, 0x6c, 0xb9, 0x33, 0x17, 0x1b, 0x7f, 0x30, 0xd0, + 0x05, 0x98, 0x17, 0x9e, 0x6d, 0x30, 0x39, 0x72, 0x73, 0x6b, 0xbe, 0x64, 0xc0, 0x59, 0x57, 0x38, + 0xb7, 0x71, 0x31, 0x0b, 0xc7, 0x07, 0x86, 0xcf, 0x06, 0xba, 0x34, 0xbe, 0x44, 0x78, 0x7b, 0xa6, + 0xcf, 0x94, 0x65, 0x34, 0x2b, 0x67, 0x50, 0x00, 0x5e, 0x59, 0xe1, 0xad, 0xe3, 0xbb, 0x59, 0x78, + 0x42, 0xaa, 0x1a, 0x43, 0xc8, 0x6f, 0x06, 0xba, 0x7a, 0x6a, 0x89, 0xf0, 0xfd, 0x99, 0xbe, 0x59, + 0x5b, 0x69, 0x3e, 0x38, 0xab, 0x0c, 0x98, 0xab, 0x8a, 0x79, 0x0b, 0x6f, 0x64, 0x31, 0x93, 0x91, + 0x74, 0xb8, 0xcc, 0xb5, 0x27, 0x47, 0x7d, 0xcb, 0x38, 0xee, 0x5b, 0xc6, 0x9f, 0xbe, 0x65, 0x7c, + 0x1c, 0x58, 0xb9, 0xe3, 0x81, 0x95, 0xfb, 0x39, 0xb0, 0x72, 0xbb, 0xe3, 0xef, 0x9b, 0xac, 0x57, + 0x8e, 0x89, 0xcf, 0x75, 0xe5, 0xd7, 0x63, 0xb5, 0xd5, 0x8b, 0xe7, 0x2f, 0xa9, 0xcf, 0xe1, 0xce, + 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x2e, 0xb3, 0x12, 0x9f, 0xe0, 0x05, 0x00, 0x00, +>>>>>>> 0efe7f22 (feat(community): add AnnualizedRewards grpc query (#1751)) } // Reference imports to suppress errors if they are not otherwise used. @@ -255,6 +377,9 @@ type QueryClient interface { // TotalBalance queries the balance of all coins, including x/distribution, // x/community, and supplied balances. TotalBalance(ctx context.Context, in *QueryTotalBalanceRequest, opts ...grpc.CallOption) (*QueryTotalBalanceResponse, error) + // AnnualizedRewards calculates and returns the current annualized reward percentages, + // like staking rewards, for the chain. + AnnualizedRewards(ctx context.Context, in *QueryAnnualizedRewardsRequest, opts ...grpc.CallOption) (*QueryAnnualizedRewardsResponse, error) } type queryClient struct { @@ -283,6 +408,15 @@ func (c *queryClient) TotalBalance(ctx context.Context, in *QueryTotalBalanceReq return out, nil } +func (c *queryClient) AnnualizedRewards(ctx context.Context, in *QueryAnnualizedRewardsRequest, opts ...grpc.CallOption) (*QueryAnnualizedRewardsResponse, error) { + out := new(QueryAnnualizedRewardsResponse) + err := c.cc.Invoke(ctx, "/kava.community.v1beta1.Query/AnnualizedRewards", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // QueryServer is the server API for Query service. type QueryServer interface { // Balance queries the balance of all coins of x/community module. @@ -290,6 +424,9 @@ type QueryServer interface { // TotalBalance queries the balance of all coins, including x/distribution, // x/community, and supplied balances. TotalBalance(context.Context, *QueryTotalBalanceRequest) (*QueryTotalBalanceResponse, error) + // AnnualizedRewards calculates and returns the current annualized reward percentages, + // like staking rewards, for the chain. + AnnualizedRewards(context.Context, *QueryAnnualizedRewardsRequest) (*QueryAnnualizedRewardsResponse, error) } // UnimplementedQueryServer can be embedded to have forward compatible implementations. @@ -302,6 +439,9 @@ func (*UnimplementedQueryServer) Balance(ctx context.Context, req *QueryBalanceR func (*UnimplementedQueryServer) TotalBalance(ctx context.Context, req *QueryTotalBalanceRequest) (*QueryTotalBalanceResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method TotalBalance not implemented") } +func (*UnimplementedQueryServer) AnnualizedRewards(ctx context.Context, req *QueryAnnualizedRewardsRequest) (*QueryAnnualizedRewardsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method AnnualizedRewards not implemented") +} func RegisterQueryServer(s grpc1.Server, srv QueryServer) { s.RegisterService(&_Query_serviceDesc, srv) @@ -343,6 +483,24 @@ func _Query_TotalBalance_Handler(srv interface{}, ctx context.Context, dec func( return interceptor(ctx, in, info, handler) } +func _Query_AnnualizedRewards_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(QueryAnnualizedRewardsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(QueryServer).AnnualizedRewards(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/kava.community.v1beta1.Query/AnnualizedRewards", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(QueryServer).AnnualizedRewards(ctx, req.(*QueryAnnualizedRewardsRequest)) + } + return interceptor(ctx, in, info, handler) +} + var _Query_serviceDesc = grpc.ServiceDesc{ ServiceName: "kava.community.v1beta1.Query", HandlerType: (*QueryServer)(nil), @@ -355,6 +513,10 @@ var _Query_serviceDesc = grpc.ServiceDesc{ MethodName: "TotalBalance", Handler: _Query_TotalBalance_Handler, }, + { + MethodName: "AnnualizedRewards", + Handler: _Query_AnnualizedRewards_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "kava/community/v1beta1/query.proto", @@ -480,6 +642,62 @@ func (m *QueryTotalBalanceResponse) MarshalToSizedBuffer(dAtA []byte) (int, erro return len(dAtA) - i, nil } +func (m *QueryAnnualizedRewardsRequest) 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 *QueryAnnualizedRewardsRequest) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryAnnualizedRewardsRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + return len(dAtA) - i, nil +} + +func (m *QueryAnnualizedRewardsResponse) 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 *QueryAnnualizedRewardsResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *QueryAnnualizedRewardsResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size := m.StakingRewards.Size() + i -= size + if _, err := m.StakingRewards.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintQuery(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + func encodeVarintQuery(dAtA []byte, offset int, v uint64) int { offset -= sovQuery(v) base := offset @@ -539,6 +757,26 @@ func (m *QueryTotalBalanceResponse) Size() (n int) { return n } +func (m *QueryAnnualizedRewardsRequest) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + return n +} + +func (m *QueryAnnualizedRewardsResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.StakingRewards.Size() + n += 1 + l + sovQuery(uint64(l)) + return n +} + func sovQuery(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -813,6 +1051,140 @@ func (m *QueryTotalBalanceResponse) Unmarshal(dAtA []byte) error { } return nil } +func (m *QueryAnnualizedRewardsRequest) 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 ErrIntOverflowQuery + } + 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: QueryAnnualizedRewardsRequest: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryAnnualizedRewardsRequest: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *QueryAnnualizedRewardsResponse) 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 ErrIntOverflowQuery + } + 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: QueryAnnualizedRewardsResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: QueryAnnualizedRewardsResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field StakingRewards", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowQuery + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthQuery + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthQuery + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.StakingRewards.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipQuery(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthQuery + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func skipQuery(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/x/community/types/query.pb.gw.go b/x/community/types/query.pb.gw.go index 9f649a3168..4cc2d79120 100644 --- a/x/community/types/query.pb.gw.go +++ b/x/community/types/query.pb.gw.go @@ -69,6 +69,24 @@ func local_request_Query_TotalBalance_0(ctx context.Context, marshaler runtime.M } +func request_Query_AnnualizedRewards_0(ctx context.Context, marshaler runtime.Marshaler, client QueryClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryAnnualizedRewardsRequest + var metadata runtime.ServerMetadata + + msg, err := client.AnnualizedRewards(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_Query_AnnualizedRewards_0(ctx context.Context, marshaler runtime.Marshaler, server QueryServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq QueryAnnualizedRewardsRequest + var metadata runtime.ServerMetadata + + msg, err := server.AnnualizedRewards(ctx, &protoReq) + return msg, metadata, err + +} + // RegisterQueryHandlerServer registers the http handlers for service Query to "mux". // UnaryRPC :call QueryServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. @@ -121,6 +139,29 @@ func RegisterQueryHandlerServer(ctx context.Context, mux *runtime.ServeMux, serv }) + mux.Handle("GET", pattern_Query_AnnualizedRewards_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_Query_AnnualizedRewards_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_AnnualizedRewards_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -202,6 +243,26 @@ func RegisterQueryHandlerClient(ctx context.Context, mux *runtime.ServeMux, clie }) + mux.Handle("GET", pattern_Query_AnnualizedRewards_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_Query_AnnualizedRewards_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_Query_AnnualizedRewards_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + + }) + return nil } @@ -209,10 +270,14 @@ var ( pattern_Query_Balance_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"kava", "community", "v1beta1", "balance"}, "", runtime.AssumeColonVerbOpt(false))) pattern_Query_TotalBalance_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"kava", "community", "v1beta1", "total_balance"}, "", runtime.AssumeColonVerbOpt(false))) + + pattern_Query_AnnualizedRewards_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"kava", "community", "v1beta1", "annualized_rewards"}, "", runtime.AssumeColonVerbOpt(false))) ) var ( forward_Query_Balance_0 = runtime.ForwardResponseMessage forward_Query_TotalBalance_0 = runtime.ForwardResponseMessage + + forward_Query_AnnualizedRewards_0 = runtime.ForwardResponseMessage )