mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-23 23:05:18 +00:00
fcfcd36740
* add total supply endpoint to x/liquid * add test of x/liquid total supply query * refactor x/savings test * add total supply endpoint for x/savings (w/ test) * add total supply endpoint for x/earn * handle converting bkava to underlying staked amount * aggregate bkava underlying values in x/earn * aggregate underlying value of bkava in x/savings
570 lines
16 KiB
Go
570 lines
16 KiB
Go
package keeper
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
|
|
"github.com/kava-labs/kava/x/earn/types"
|
|
)
|
|
|
|
type queryServer struct {
|
|
keeper Keeper
|
|
}
|
|
|
|
// NewQueryServerImpl creates a new server for handling gRPC queries.
|
|
func NewQueryServerImpl(k Keeper) types.QueryServer {
|
|
return &queryServer{keeper: k}
|
|
}
|
|
|
|
var _ types.QueryServer = queryServer{}
|
|
|
|
// Params implements the gRPC service handler for querying x/earn parameters.
|
|
func (s queryServer) Params(
|
|
ctx context.Context,
|
|
req *types.QueryParamsRequest,
|
|
) (*types.QueryParamsResponse, error) {
|
|
if req == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "empty request")
|
|
}
|
|
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
params := s.keeper.GetParams(sdkCtx)
|
|
|
|
return &types.QueryParamsResponse{Params: params}, nil
|
|
}
|
|
|
|
// Vaults implements the gRPC service handler for querying x/earn vaults.
|
|
func (s queryServer) Vaults(
|
|
ctx context.Context,
|
|
req *types.QueryVaultsRequest,
|
|
) (*types.QueryVaultsResponse, error) {
|
|
if req == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "empty request")
|
|
}
|
|
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
|
|
allowedVaults := s.keeper.GetAllowedVaults(sdkCtx)
|
|
allowedVaultsMap := make(map[string]types.AllowedVault)
|
|
visitedMap := make(map[string]bool)
|
|
for _, av := range allowedVaults {
|
|
allowedVaultsMap[av.Denom] = av
|
|
visitedMap[av.Denom] = false
|
|
}
|
|
|
|
vaults := []types.VaultResponse{}
|
|
|
|
var vaultRecordsErr error
|
|
|
|
// Iterate over vault records instead of AllowedVaults to get all bkava-*
|
|
// vaults
|
|
s.keeper.IterateVaultRecords(sdkCtx, func(record types.VaultRecord) bool {
|
|
// Check if bkava, use allowed vault
|
|
allowedVaultDenom := record.TotalShares.Denom
|
|
if strings.HasPrefix(record.TotalShares.Denom, bkavaPrefix) {
|
|
allowedVaultDenom = bkavaDenom
|
|
}
|
|
|
|
allowedVault, found := allowedVaultsMap[allowedVaultDenom]
|
|
if !found {
|
|
vaultRecordsErr = fmt.Errorf("vault record not found for vault record denom %s", record.TotalShares.Denom)
|
|
return true
|
|
}
|
|
|
|
totalValue, err := s.keeper.GetVaultTotalValue(sdkCtx, record.TotalShares.Denom)
|
|
if err != nil {
|
|
vaultRecordsErr = err
|
|
// Stop iterating if error
|
|
return true
|
|
}
|
|
|
|
vaults = append(vaults, types.VaultResponse{
|
|
Denom: record.TotalShares.Denom,
|
|
Strategies: allowedVault.Strategies,
|
|
IsPrivateVault: allowedVault.IsPrivateVault,
|
|
AllowedDepositors: addressSliceToStringSlice(allowedVault.AllowedDepositors),
|
|
TotalShares: record.TotalShares.Amount.String(),
|
|
TotalValue: totalValue.Amount,
|
|
})
|
|
|
|
// Mark this allowed vault as visited
|
|
visitedMap[allowedVaultDenom] = true
|
|
|
|
return false
|
|
})
|
|
|
|
if vaultRecordsErr != nil {
|
|
return nil, vaultRecordsErr
|
|
}
|
|
|
|
// Add the allowed vaults that have not been visited yet
|
|
// These are always empty vaults, as the vault would have been visited
|
|
// earlier if there are any deposits
|
|
for denom, visited := range visitedMap {
|
|
if visited {
|
|
continue
|
|
}
|
|
|
|
allowedVault, found := allowedVaultsMap[denom]
|
|
if !found {
|
|
return nil, fmt.Errorf("vault record not found for vault record denom %s", denom)
|
|
}
|
|
|
|
vaults = append(vaults, types.VaultResponse{
|
|
Denom: denom,
|
|
Strategies: allowedVault.Strategies,
|
|
IsPrivateVault: allowedVault.IsPrivateVault,
|
|
AllowedDepositors: addressSliceToStringSlice(allowedVault.AllowedDepositors),
|
|
// No shares, no value
|
|
TotalShares: sdk.ZeroDec().String(),
|
|
TotalValue: sdk.ZeroInt(),
|
|
})
|
|
}
|
|
|
|
// Does not include vaults that have no deposits, only iterates over vault
|
|
// records which exists only for those with deposits.
|
|
return &types.QueryVaultsResponse{
|
|
Vaults: vaults,
|
|
}, nil
|
|
}
|
|
|
|
// Vaults implements the gRPC service handler for querying x/earn vaults.
|
|
func (s queryServer) Vault(
|
|
ctx context.Context,
|
|
req *types.QueryVaultRequest,
|
|
) (*types.QueryVaultResponse, error) {
|
|
if req == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "empty request")
|
|
}
|
|
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
|
|
if req.Denom == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "empty denom")
|
|
}
|
|
|
|
// Only 1 vault
|
|
allowedVault, found := s.keeper.GetAllowedVault(sdkCtx, req.Denom)
|
|
if !found {
|
|
return nil, status.Errorf(codes.NotFound, "vault not found with specified denom")
|
|
}
|
|
|
|
// Handle bkava separately to get total of **all** bkava vaults
|
|
if req.Denom == bkavaDenom {
|
|
return s.getAggregateBkavaVault(sdkCtx, allowedVault)
|
|
}
|
|
|
|
// Must be req.Denom and not allowedVault.Denom to get full "bkava" denom
|
|
vaultRecord, found := s.keeper.GetVaultRecord(sdkCtx, req.Denom)
|
|
if !found {
|
|
// No supply yet, no error just set it to zero
|
|
vaultRecord.TotalShares = types.NewVaultShare(req.Denom, sdk.ZeroDec())
|
|
}
|
|
|
|
totalValue, err := s.keeper.GetVaultTotalValue(sdkCtx, req.Denom)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
vault := types.VaultResponse{
|
|
// VaultRecord denom instead of AllowedVault.Denom for full bkava denom
|
|
Denom: vaultRecord.TotalShares.Denom,
|
|
Strategies: allowedVault.Strategies,
|
|
IsPrivateVault: allowedVault.IsPrivateVault,
|
|
AllowedDepositors: addressSliceToStringSlice(allowedVault.AllowedDepositors),
|
|
TotalShares: vaultRecord.TotalShares.Amount.String(),
|
|
TotalValue: totalValue.Amount,
|
|
}
|
|
|
|
return &types.QueryVaultResponse{
|
|
Vault: vault,
|
|
}, nil
|
|
}
|
|
|
|
// getAggregateBkavaVault returns a VaultResponse of the total of all bkava
|
|
// vaults.
|
|
func (s queryServer) getAggregateBkavaVault(
|
|
ctx sdk.Context,
|
|
allowedVault types.AllowedVault,
|
|
) (*types.QueryVaultResponse, error) {
|
|
allBkava := sdk.NewCoins()
|
|
|
|
var iterErr error
|
|
s.keeper.IterateVaultRecords(ctx, func(record types.VaultRecord) (stop bool) {
|
|
// Skip non bkava vaults
|
|
if !strings.HasPrefix(record.TotalShares.Denom, bkavaPrefix) {
|
|
return false
|
|
}
|
|
|
|
vaultValue, err := s.keeper.GetVaultTotalValue(ctx, record.TotalShares.Denom)
|
|
if err != nil {
|
|
iterErr = err
|
|
return false
|
|
}
|
|
|
|
allBkava = allBkava.Add(vaultValue)
|
|
|
|
return false
|
|
})
|
|
|
|
if iterErr != nil {
|
|
return nil, iterErr
|
|
}
|
|
|
|
vaultValue, err := s.keeper.liquidKeeper.GetStakedTokensForDerivatives(ctx, allBkava)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &types.QueryVaultResponse{
|
|
Vault: types.VaultResponse{
|
|
Denom: bkavaDenom,
|
|
Strategies: allowedVault.Strategies,
|
|
IsPrivateVault: allowedVault.IsPrivateVault,
|
|
AllowedDepositors: addressSliceToStringSlice(allowedVault.AllowedDepositors),
|
|
// Empty for shares, as adding up all shares is not useful information
|
|
TotalShares: "0",
|
|
TotalValue: vaultValue.Amount,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Deposits implements the gRPC service handler for querying x/earn deposits.
|
|
func (s queryServer) Deposits(
|
|
ctx context.Context,
|
|
req *types.QueryDepositsRequest,
|
|
) (*types.QueryDepositsResponse, error) {
|
|
if req == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "empty request")
|
|
}
|
|
|
|
if req.Depositor == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "depositor is required")
|
|
}
|
|
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
|
|
// bkava aggregate total
|
|
if req.Denom == bkavaDenom {
|
|
return s.getOneAccountBkavaVaultDeposit(sdkCtx, req)
|
|
}
|
|
|
|
// specific vault
|
|
if req.Denom != "" {
|
|
return s.getOneAccountOneVaultDeposit(sdkCtx, req)
|
|
}
|
|
|
|
// all vaults
|
|
return s.getOneAccountAllDeposits(sdkCtx, req)
|
|
}
|
|
|
|
// TotalSupply implements the gRPC service handler for querying x/earn total supply (TVL)
|
|
func (s queryServer) TotalSupply(
|
|
ctx context.Context,
|
|
req *types.QueryTotalSupplyRequest,
|
|
) (*types.QueryTotalSupplyResponse, error) {
|
|
sdkCtx := sdk.UnwrapSDKContext(ctx)
|
|
totalSupply := sdk.NewCoins()
|
|
liquidStakedDerivatives := sdk.NewCoins()
|
|
|
|
// allowed vaults param contains info on allowed strategies, but bkava is aggregated
|
|
allowedVaults := s.keeper.GetAllowedVaults(sdkCtx)
|
|
allowedVaultByDenom := make(map[string]types.AllowedVault)
|
|
for _, av := range allowedVaults {
|
|
allowedVaultByDenom[av.Denom] = av
|
|
}
|
|
|
|
var vaultRecordErr error
|
|
// iterate actual records to properly enumerate all denoms
|
|
s.keeper.IterateVaultRecords(sdkCtx, func(vault types.VaultRecord) (stop bool) {
|
|
isLiquidStakingDenom := false
|
|
// find allowed vault to get parameters. handle translating bkava denoms to allowed vault denom
|
|
allowedVaultDenom := vault.TotalShares.Denom
|
|
if strings.HasPrefix(vault.TotalShares.Denom, bkavaPrefix) {
|
|
isLiquidStakingDenom = true
|
|
allowedVaultDenom = bkavaDenom
|
|
}
|
|
allowedVault, found := allowedVaultByDenom[allowedVaultDenom]
|
|
if !found {
|
|
vaultRecordErr = fmt.Errorf("vault record not found for vault record denom %s", vault.TotalShares.Denom)
|
|
return true
|
|
}
|
|
|
|
// only consider savings strategy vaults when determining supply
|
|
if !allowedVault.IsStrategyAllowed(types.STRATEGY_TYPE_SAVINGS) {
|
|
return false
|
|
}
|
|
|
|
// vault has savings strategy! determine total value of vault and add to sum
|
|
vaultSupply, err := s.keeper.GetVaultTotalValue(sdkCtx, vault.TotalShares.Denom)
|
|
if err != nil {
|
|
vaultRecordErr = err
|
|
return true
|
|
}
|
|
|
|
// liquid staked tokens must be converted to their underlying value
|
|
// aggregate them here and then we can convert to underlying values all at once at the end
|
|
if isLiquidStakingDenom {
|
|
liquidStakedDerivatives = liquidStakedDerivatives.Add(vaultSupply)
|
|
} else {
|
|
totalSupply = totalSupply.Add(vaultSupply)
|
|
}
|
|
return false
|
|
})
|
|
|
|
// determine underlying value of bkava denoms
|
|
if len(liquidStakedDerivatives) > 0 {
|
|
underlyingValue, err := s.keeper.liquidKeeper.GetStakedTokensForDerivatives(
|
|
sdkCtx,
|
|
liquidStakedDerivatives,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
totalSupply = totalSupply.Add(sdk.NewCoin(bkavaDenom, underlyingValue.Amount))
|
|
}
|
|
|
|
return &types.QueryTotalSupplyResponse{
|
|
Height: sdkCtx.BlockHeight(),
|
|
Result: totalSupply,
|
|
}, vaultRecordErr
|
|
}
|
|
|
|
// getOneAccountOneVaultDeposit returns deposits for a specific vault and a specific
|
|
// account
|
|
func (s queryServer) getOneAccountOneVaultDeposit(
|
|
ctx sdk.Context,
|
|
req *types.QueryDepositsRequest,
|
|
) (*types.QueryDepositsResponse, error) {
|
|
depositor, err := sdk.AccAddressFromBech32(req.Depositor)
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, "Invalid address")
|
|
}
|
|
|
|
shareRecord, found := s.keeper.GetVaultShareRecord(ctx, depositor)
|
|
if !found {
|
|
return &types.QueryDepositsResponse{
|
|
Deposits: []types.DepositResponse{
|
|
{
|
|
Depositor: depositor.String(),
|
|
// Zero shares and zero value for no deposits
|
|
Shares: types.NewVaultShares(types.NewVaultShare(req.Denom, sdk.ZeroDec())),
|
|
Value: sdk.NewCoins(sdk.NewCoin(req.Denom, sdk.ZeroInt())),
|
|
},
|
|
},
|
|
Pagination: nil,
|
|
}, nil
|
|
}
|
|
|
|
// Only requesting the value of the specified denom
|
|
value, err := s.keeper.GetVaultAccountValue(ctx, req.Denom, depositor)
|
|
if err != nil {
|
|
return nil, status.Error(codes.NotFound, err.Error())
|
|
}
|
|
|
|
if req.ValueInStakedTokens {
|
|
// Get underlying ukava amount if denom is a derivative
|
|
if !s.keeper.liquidKeeper.IsDerivativeDenom(ctx, req.Denom) {
|
|
return nil, status.Errorf(
|
|
codes.InvalidArgument,
|
|
"denom %s is not a derivative, ValueInStakedTokens can only be used with liquid derivatives",
|
|
req.Denom,
|
|
)
|
|
}
|
|
|
|
ukavaValue, err := s.keeper.liquidKeeper.GetStakedTokensForDerivatives(ctx, sdk.NewCoins(value))
|
|
if err != nil {
|
|
// This should "never" happen if IsDerivativeDenom is true
|
|
panic("Error getting ukava value for " + req.Denom)
|
|
}
|
|
|
|
value = ukavaValue
|
|
}
|
|
|
|
return &types.QueryDepositsResponse{
|
|
Deposits: []types.DepositResponse{
|
|
{
|
|
Depositor: depositor.String(),
|
|
// Only respond with requested denom shares
|
|
Shares: types.NewVaultShares(
|
|
types.NewVaultShare(req.Denom, shareRecord.Shares.AmountOf(req.Denom)),
|
|
),
|
|
Value: sdk.NewCoins(value),
|
|
},
|
|
},
|
|
Pagination: nil,
|
|
}, nil
|
|
}
|
|
|
|
// getOneAccountBkavaVaultDeposit returns deposits for the aggregated bkava vault
|
|
// and a specific account
|
|
func (s queryServer) getOneAccountBkavaVaultDeposit(
|
|
ctx sdk.Context,
|
|
req *types.QueryDepositsRequest,
|
|
) (*types.QueryDepositsResponse, error) {
|
|
depositor, err := sdk.AccAddressFromBech32(req.Depositor)
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, "Invalid address")
|
|
}
|
|
|
|
shareRecord, found := s.keeper.GetVaultShareRecord(ctx, depositor)
|
|
if !found {
|
|
return &types.QueryDepositsResponse{
|
|
Deposits: []types.DepositResponse{
|
|
{
|
|
Depositor: depositor.String(),
|
|
// Zero shares and zero value for no deposits
|
|
Shares: types.NewVaultShares(types.NewVaultShare(req.Denom, sdk.ZeroDec())),
|
|
Value: sdk.NewCoins(sdk.NewCoin(req.Denom, sdk.ZeroInt())),
|
|
},
|
|
},
|
|
Pagination: nil,
|
|
}, nil
|
|
}
|
|
|
|
// Get all account deposit values to add up bkava
|
|
totalAccountValue, err := getAccountTotalValue(ctx, s.keeper, depositor, shareRecord.Shares)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Remove non-bkava coins, GetStakedTokensForDerivatives expects only bkava
|
|
totalBkavaValue := sdk.NewCoins()
|
|
for _, coin := range totalAccountValue {
|
|
if s.keeper.liquidKeeper.IsDerivativeDenom(ctx, coin.Denom) {
|
|
totalBkavaValue = totalBkavaValue.Add(coin)
|
|
}
|
|
}
|
|
|
|
// Use account value with only the aggregate bkava converted to underlying staked tokens
|
|
stakedValue, err := s.keeper.liquidKeeper.GetStakedTokensForDerivatives(ctx, totalBkavaValue)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &types.QueryDepositsResponse{
|
|
Deposits: []types.DepositResponse{
|
|
{
|
|
Depositor: depositor.String(),
|
|
// Only respond with requested denom shares
|
|
Shares: types.NewVaultShares(
|
|
types.NewVaultShare(req.Denom, shareRecord.Shares.AmountOf(req.Denom)),
|
|
),
|
|
Value: sdk.NewCoins(stakedValue),
|
|
},
|
|
},
|
|
Pagination: nil,
|
|
}, nil
|
|
}
|
|
|
|
// getOneAccountAllDeposits returns deposits for all vaults for a specific account
|
|
func (s queryServer) getOneAccountAllDeposits(
|
|
ctx sdk.Context,
|
|
req *types.QueryDepositsRequest,
|
|
) (*types.QueryDepositsResponse, error) {
|
|
depositor, err := sdk.AccAddressFromBech32(req.Depositor)
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, "Invalid address")
|
|
}
|
|
|
|
deposits := []types.DepositResponse{}
|
|
|
|
accountShare, found := s.keeper.GetVaultShareRecord(ctx, depositor)
|
|
if !found {
|
|
return &types.QueryDepositsResponse{
|
|
Deposits: []types.DepositResponse{},
|
|
Pagination: nil,
|
|
}, nil
|
|
}
|
|
|
|
value, err := getAccountTotalValue(ctx, s.keeper, depositor, accountShare.Shares)
|
|
if err != nil {
|
|
return nil, status.Error(codes.InvalidArgument, err.Error())
|
|
}
|
|
|
|
if req.ValueInStakedTokens {
|
|
// Plain slice to not sum ukava amounts together. This is not a valid
|
|
// sdk.Coin due to multiple coins of the same denom, but we need them to
|
|
// be separate in the response to not be an aggregate amount.
|
|
var valueInStakedTokens []sdk.Coin
|
|
|
|
for _, coin := range value {
|
|
// Skip non-bkava coins
|
|
if !s.keeper.liquidKeeper.IsDerivativeDenom(ctx, coin.Denom) {
|
|
continue
|
|
}
|
|
|
|
// Derivative coins are converted to underlying staked tokens
|
|
ukavaValue, err := s.keeper.liquidKeeper.GetStakedTokensForDerivatives(ctx, sdk.NewCoins(coin))
|
|
if err != nil {
|
|
// This should "never" happen if IsDerivativeDenom is true
|
|
panic("Error getting ukava value for " + coin.Denom)
|
|
}
|
|
valueInStakedTokens = append(valueInStakedTokens, ukavaValue)
|
|
}
|
|
|
|
var filteredShares types.VaultShares
|
|
for _, share := range accountShare.Shares {
|
|
// Remove non-bkava coins from shares as they are used to
|
|
// determine which value is mapped to which denom
|
|
// These should be in the same order as valueInStakedTokens
|
|
if !s.keeper.liquidKeeper.IsDerivativeDenom(ctx, share.Denom) {
|
|
continue
|
|
}
|
|
|
|
filteredShares = append(filteredShares, share)
|
|
}
|
|
|
|
value = valueInStakedTokens
|
|
accountShare.Shares = filteredShares
|
|
}
|
|
|
|
deposits = append(deposits, types.DepositResponse{
|
|
Depositor: depositor.String(),
|
|
Shares: accountShare.Shares,
|
|
Value: value,
|
|
})
|
|
|
|
return &types.QueryDepositsResponse{
|
|
Deposits: deposits,
|
|
Pagination: nil,
|
|
}, nil
|
|
}
|
|
|
|
// getAccountTotalValue returns the total value for all vaults for a specific
|
|
// account based on their shares.
|
|
func getAccountTotalValue(
|
|
ctx sdk.Context,
|
|
keeper Keeper,
|
|
account sdk.AccAddress,
|
|
shares types.VaultShares,
|
|
) (sdk.Coins, error) {
|
|
value := sdk.NewCoins()
|
|
|
|
for _, share := range shares {
|
|
accValue, err := keeper.GetVaultAccountValue(ctx, share.Denom, account)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
value = value.Add(sdk.NewCoin(share.Denom, accValue.Amount))
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
func addressSliceToStringSlice(addresses []sdk.AccAddress) []string {
|
|
var strings []string
|
|
for _, address := range addresses {
|
|
strings = append(strings, address.String())
|
|
}
|
|
|
|
return strings
|
|
}
|