Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: sf undelegating / delegated cl queries #5691

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions proto/osmosis/superfluid/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,20 @@ service Query {
"unpool_whitelist";
}

rpc UserSuperfluidPositionsPerConcentratedPoolBreakdown(
UserSuperfluidPositionsPerConcentratedPoolBreakdownRequest)
returns (UserSuperfluidPositionsPerConcentratedPoolBreakdownResponse) {
rpc UserConcentratedSuperfluidPositionsBonded(
UserConcentratedSuperfluidPositionsBondedRequest)
jonator marked this conversation as resolved.
Show resolved Hide resolved
returns (UserConcentratedSuperfluidPositionsBondedResponse) {
option (google.api.http).get = "/osmosis/superfluid/v1beta1/"
"user_superfluid_positions_per_pool/"
"{delegator_address}/{concentrated_pool_id}";
"account_delegated_cl_positions/"
"{delegator_address}";
}

rpc UserConcentratedSuperfluidPositionsUnbonding(
UserConcentratedSuperfluidPositionsUnbondingRequest)
returns (UserConcentratedSuperfluidPositionsUnbondingResponse) {
option (google.api.http).get = "/osmosis/superfluid/v1beta1/"
"account_undelegating_cl_positions/"
"{delegator_address}";
}
}

Expand Down Expand Up @@ -291,15 +299,20 @@ message QueryUnpoolWhitelistRequest {}

message QueryUnpoolWhitelistResponse { repeated uint64 pool_ids = 1; }

//===============================
// UserSuperfluidPositionsPerConcentratedPoolBreakdown
message UserSuperfluidPositionsPerConcentratedPoolBreakdownRequest {
message UserConcentratedSuperfluidPositionsBondedRequest {
string delegator_address = 1;
}

message UserConcentratedSuperfluidPositionsBondedResponse {
repeated ConcentratedPoolUserPositionRecord cl_pool_user_position_records = 1
[ (gogoproto.nullable) = false ];
}

message UserConcentratedSuperfluidPositionsUnbondingRequest {
string delegator_address = 1;
uint64 concentrated_pool_id = 2
[ (gogoproto.moretags) = "yaml:\"concentrated_pool_id\"" ];
}

message UserSuperfluidPositionsPerConcentratedPoolBreakdownResponse {
message UserConcentratedSuperfluidPositionsUnbondingResponse {
repeated ConcentratedPoolUserPositionRecord cl_pool_user_position_records = 1
[ (gogoproto.nullable) = false ];
}
152 changes: 100 additions & 52 deletions x/superfluid/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package keeper

import (
"context"
"fmt"
"errors"
"strings"
"time"

Expand All @@ -19,6 +19,7 @@ import (
"github.com/cosmos/cosmos-sdk/store/prefix"
"github.com/cosmos/cosmos-sdk/types/query"

"github.com/osmosis-labs/osmosis/v16/x/concentrated-liquidity/model"
cltypes "github.com/osmosis-labs/osmosis/v16/x/concentrated-liquidity/types"
lockuptypes "github.com/osmosis-labs/osmosis/v16/x/lockup/types"
"github.com/osmosis-labs/osmosis/v16/x/superfluid/types"
Expand Down Expand Up @@ -280,77 +281,56 @@ func (q Querier) SuperfluidDelegationsByDelegator(goCtx context.Context, req *ty
return &res, nil
}

// UserSuperfluidPositionsPerConcentratedPoolBreakdown returns all the cl superfluid positions for the specified delegator in the specified concentrated liquidity pool.
func (q Querier) UserSuperfluidPositionsPerConcentratedPoolBreakdown(goCtx context.Context, req *types.UserSuperfluidPositionsPerConcentratedPoolBreakdownRequest) (*types.UserSuperfluidPositionsPerConcentratedPoolBreakdownResponse, error) {
// UserConcentratedSuperfluidPositionsBonded returns all the cl superfluid positions for the specified delegator across all concentrated pools that are bonded.
func (q Querier) UserConcentratedSuperfluidPositionsBonded(goCtx context.Context, req *types.UserConcentratedSuperfluidPositionsBondedRequest) (*types.UserConcentratedSuperfluidPositionsBondedResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

delAddr, err := sdk.AccAddressFromBech32(req.DelegatorAddress)
if err != nil {
return nil, err
}

// Get the position IDs for the specified pool ID and user address.
positions, err := q.Keeper.clk.GetUserPositions(ctx, delAddr, req.ConcentratedPoolId)
// Get the position IDs across all pools for the given user address.
positions, err := q.Keeper.clk.GetUserPositions(ctx, delAddr, 0)
if err != nil {
return nil, err
}

// Query each position ID and determine if it has a lock ID associated with it.
// Construct a response with the position ID, lock ID, the amount of cl shares staked, and what those shares are worth in staked osmo tokens.
var clPoolUserPositionRecords []types.ConcentratedPoolUserPositionRecord
for _, pos := range positions {
lockId, err := q.Keeper.clk.GetLockIdFromPositionId(ctx, pos.PositionId)
switch err.(type) {
case cltypes.PositionIdToLockNotFoundError:
continue
case nil:
// If we have hit this logic branch, it means that, at one point, the lockId provided existed. If we fetch it again
// and it doesn't exist, that means that the lock has matured.
lock, err := q.Keeper.lk.GetLockByID(ctx, lockId)
if err == errorsmod.Wrap(lockuptypes.ErrLockupNotFound, fmt.Sprintf("lock with ID %d does not exist", lock.GetID())) {
continue
}
if err != nil {
return nil, err
}
clPoolUserPositionRecords, err := q.filterConcentratedPositionLocks(ctx, positions, false)
if err != nil {
return nil, err
}

syntheticLock, err := q.Keeper.lk.GetSyntheticLockupByUnderlyingLockId(ctx, lockId)
if err != nil {
return nil, err
}
return &types.UserConcentratedSuperfluidPositionsBondedResponse{
ClPoolUserPositionRecords: clPoolUserPositionRecords,
}, nil
}

// Its possible for a non superfluid lock to be attached to a position. This can happen for users migrating non superfluid positions that
// they intend to let mature so they can eventually set non full range positions.
if syntheticLock.UnderlyingLockId == 0 {
continue
}
// UserConcentratedSuperfluidPositionsUnbonding returns all the cl superfluid positions for the specified delegator across all concentrated pools that are unbonding.
func (q Querier) UserConcentratedSuperfluidPositionsUnbonding(goCtx context.Context, req *types.UserConcentratedSuperfluidPositionsUnbondingRequest) (*types.UserConcentratedSuperfluidPositionsUnbondingResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)

valAddr, err := ValidatorAddressFromSyntheticDenom(syntheticLock.SynthDenom)
if err != nil {
return nil, err
}
delAddr, err := sdk.AccAddressFromBech32(req.DelegatorAddress)
if err != nil {
return nil, err
}

baseDenom := lock.Coins.GetDenomByIndex(0)
lockedCoins := sdk.NewCoin(baseDenom, lock.GetCoins().AmountOf(baseDenom))
equivalentAmount, err := q.Keeper.GetSuperfluidOSMOTokens(ctx, baseDenom, lockedCoins.Amount)
if err != nil {
return nil, err
}
coin := sdk.NewCoin(appparams.BaseCoinUnit, equivalentAmount)
// Get the position IDs across all pools for the given user address.
positions, err := q.Keeper.clk.GetUserPositions(ctx, delAddr, 0)
if err != nil {
return nil, err
}

clPoolUserPositionRecords = append(clPoolUserPositionRecords, types.ConcentratedPoolUserPositionRecord{
ValidatorAddress: valAddr,
PositionId: pos.PositionId,
LockId: lockId,
DelegationAmount: lockedCoins,
EquivalentStakedAmount: &coin,
})
default:
continue
}
// Query each position ID and determine if it has a lock ID associated with it.
// Construct a response with the position ID, lock ID, the amount of cl shares staked, and what those shares are worth in staked osmo tokens.
clPoolUserPositionRecords, err := q.filterConcentratedPositionLocks(ctx, positions, true)
if err != nil {
return nil, err
}

return &types.UserSuperfluidPositionsPerConcentratedPoolBreakdownResponse{
return &types.UserConcentratedSuperfluidPositionsUnbondingResponse{
ClPoolUserPositionRecords: clPoolUserPositionRecords,
}, nil
}
Expand Down Expand Up @@ -653,3 +633,71 @@ func (q Querier) UnpoolWhitelist(goCtx context.Context, req *types.QueryUnpoolWh
PoolIds: allowedPools,
}, nil
}

func (q Querier) filterConcentratedPositionLocks(ctx sdk.Context, positions []model.Position, isUnbonding bool) ([]types.ConcentratedPoolUserPositionRecord, error) {
// Query each position ID and determine if it has a lock ID associated with it.
// Construct a response with the position ID, lock ID, the amount of cl shares staked, and what those shares are worth in staked osmo tokens.
var clPoolUserPositionRecords []types.ConcentratedPoolUserPositionRecord
for _, pos := range positions {
lockId, err := q.Keeper.clk.GetLockIdFromPositionId(ctx, pos.PositionId)
if errors.Is(err, cltypes.PositionIdToLockNotFoundError{}) {
continue
} else if err != nil {
return nil, err
}

// If we have hit this logic branch, it means that, at one point, the lockId provided existed. If we fetch it again
// and it doesn't exist, that means that the lock has matured.
lock, err := q.Keeper.lk.GetLockByID(ctx, lockId)
if errors.Is(err, lockuptypes.ErrLockupNotFound) {
continue
} else if err != nil {
return nil, err
}

syntheticLock, err := q.Keeper.lk.GetSyntheticLockupByUnderlyingLockId(ctx, lockId)
if err != nil {
return nil, err
}

// Its possible for a non superfluid lock to be attached to a position. This can happen for users migrating non superfluid positions that
// they intend to let mature so they can eventually set non full range positions.
if syntheticLock.UnderlyingLockId == 0 {
continue
}

if isUnbonding {
// We only want to return unbonding positions.
if !strings.Contains(syntheticLock.SynthDenom, "/superunbonding") {
continue
}
} else {
// We only want to return bonding positions.
if !strings.Contains(syntheticLock.SynthDenom, "/superbonding") {
continue
}
}

valAddr, err := ValidatorAddressFromSyntheticDenom(syntheticLock.SynthDenom)
if err != nil {
return nil, err
}

baseDenom := lock.Coins.GetDenomByIndex(0)
lockedCoins := sdk.NewCoin(baseDenom, lock.GetCoins().AmountOf(baseDenom))
equivalentAmount, err := q.Keeper.GetSuperfluidOSMOTokens(ctx, baseDenom, lockedCoins.Amount)
if err != nil {
return nil, err
}
coin := sdk.NewCoin(appparams.BaseCoinUnit, equivalentAmount)

clPoolUserPositionRecords = append(clPoolUserPositionRecords, types.ConcentratedPoolUserPositionRecord{
ValidatorAddress: valAddr,
PositionId: pos.PositionId,
LockId: lockId,
DelegationAmount: lockedCoins,
EquivalentStakedAmount: &coin,
})
}
return clPoolUserPositionRecords, nil
}
88 changes: 62 additions & 26 deletions x/superfluid/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ func (s *KeeperTestSuite) TestGRPCQuerySuperfluidDelegationsDontIncludeUnbonding
s.Require().Equal(totalSuperfluidDelegationsRes.TotalDelegations, sdk.NewInt(30000000))
}

func (s *KeeperTestSuite) TestUserSuperfluidPositionsPerConcentratedPoolBreakdown() {
func (s *KeeperTestSuite) TestUserConcentratedSuperfluidPositionsBondedAndUnbonding() {
s.SetupTest()

// Setup 2 validators.
Expand Down Expand Up @@ -309,9 +309,9 @@ func (s *KeeperTestSuite) TestUserSuperfluidPositionsPerConcentratedPoolBreakdow
duration := s.App.StakingKeeper.GetParams(s.Ctx).UnbondingTime

// Create 4 positions in pool 1 that are superfluid delegated.
expectedPositionIds := []uint64{}
expectedLockIds := []uint64{}
expectedTotalSharesLocked := sdk.Coins{}
expectedBondedPositionIds := []uint64{}
expectedBondedLockIds := []uint64{}
expectedBondedTotalSharesLocked := sdk.Coins{}
for i := 0; i < 4; i++ {
posId, _, _, _, lockId, err := s.App.ConcentratedLiquidityKeeper.CreateFullRangePositionLocked(s.Ctx, clPoolId, s.TestAccs[0], coins, duration)
s.Require().NoError(err)
Expand All @@ -322,55 +322,91 @@ func (s *KeeperTestSuite) TestUserSuperfluidPositionsPerConcentratedPoolBreakdow
err = s.App.SuperfluidKeeper.SuperfluidDelegate(s.Ctx, lock.Owner, lock.ID, valAddrs[0].String())
s.Require().NoError(err)

expectedPositionIds = append(expectedPositionIds, posId)
expectedLockIds = append(expectedLockIds, lockId)
expectedTotalSharesLocked = expectedTotalSharesLocked.Add(lock.Coins[0])
expectedBondedPositionIds = append(expectedBondedPositionIds, posId)
expectedBondedLockIds = append(expectedBondedLockIds, lockId)
expectedBondedTotalSharesLocked = expectedBondedTotalSharesLocked.Add(lock.Coins[0])
}

// Create 1 position in pool 1 that is not superfluid delegated.
_, _, _, _, err = s.App.ConcentratedLiquidityKeeper.CreateFullRangePosition(s.Ctx, clPoolId, s.TestAccs[0], coins)
s.Require().NoError(err)

// Create 4 positions in pool 2 that are superfluid delegated.
// Create 4 positions in pool 2 that are superfluid undelegating.
expectedUnbondingPositionIds := []uint64{}
expectedUnbondingLockIds := []uint64{}
expectedUnbondingTotalSharesLocked := sdk.Coins{}
for i := 0; i < 4; i++ {
_, _, _, _, lockId, err := s.App.ConcentratedLiquidityKeeper.CreateFullRangePositionLocked(s.Ctx, clPoolId2, s.TestAccs[0], coins, duration)
posId, _, _, _, lockId, err := s.App.ConcentratedLiquidityKeeper.CreateFullRangePositionLocked(s.Ctx, clPoolId2, s.TestAccs[0], coins, duration)
s.Require().NoError(err)

lock, err := s.App.LockupKeeper.GetLockByID(s.Ctx, lockId)
s.Require().NoError(err)

err = s.App.SuperfluidKeeper.SuperfluidDelegate(s.Ctx, lock.Owner, lock.ID, valAddrs[0].String())
s.Require().NoError(err)

_, err = s.App.SuperfluidKeeper.SuperfluidUndelegateAndUnbondLock(s.Ctx, lockId, lock.Owner, lock.Coins[0].Amount)
s.Require().NoError(err)

expectedUnbondingPositionIds = append(expectedUnbondingPositionIds, posId)
expectedUnbondingLockIds = append(expectedUnbondingLockIds, lockId)
expectedUnbondingTotalSharesLocked = expectedUnbondingTotalSharesLocked.Add(lock.Coins[0])
}

// Create 1 position in pool 2 that is not superfluid delegated.
_, _, _, _, err = s.App.ConcentratedLiquidityKeeper.CreateFullRangePosition(s.Ctx, clPoolId2, s.TestAccs[0], coins)
s.Require().NoError(err)

res, err := s.queryClient.UserSuperfluidPositionsPerConcentratedPoolBreakdown(sdk.WrapSDKContext(s.Ctx), &types.UserSuperfluidPositionsPerConcentratedPoolBreakdownRequest{
DelegatorAddress: s.TestAccs[0].String(),
ConcentratedPoolId: clPoolId,
// Query the bonded positions.
bondedRes, err := s.queryClient.UserConcentratedSuperfluidPositionsBonded(sdk.WrapSDKContext(s.Ctx), &types.UserConcentratedSuperfluidPositionsBondedRequest{
DelegatorAddress: s.TestAccs[0].String(),
})
s.Require().NoError(err)

// The result should only have the four bonded superfluid positions
s.Require().Equal(4, len(bondedRes.ClPoolUserPositionRecords))
s.Require().Equal(4, len(expectedBondedPositionIds))
s.Require().Equal(4, len(expectedBondedLockIds))

actualBondedPositionIds := []uint64{}
actualBondedLockIds := []uint64{}
actualBondedTotalSharesLocked := sdk.Coins{}
for _, record := range bondedRes.ClPoolUserPositionRecords {
s.Require().Equal(record.ValidatorAddress, valAddrs[0].String()) // User 0 only used this validator
actualBondedPositionIds = append(actualBondedPositionIds, record.PositionId)
actualBondedLockIds = append(actualBondedLockIds, record.LockId)
actualBondedTotalSharesLocked = actualBondedTotalSharesLocked.Add(record.DelegationAmount)
}

s.Require().True(osmoutils.ContainsDuplicateDeepEqual([]interface{}{expectedBondedPositionIds, actualBondedPositionIds}))
s.Require().True(osmoutils.ContainsDuplicateDeepEqual([]interface{}{expectedBondedLockIds, actualBondedLockIds}))
s.Require().Equal(expectedBondedTotalSharesLocked, actualBondedTotalSharesLocked)

// Query the unbonding positions.
unbondingRes, err := s.queryClient.UserConcentratedSuperfluidPositionsUnbonding(sdk.WrapSDKContext(s.Ctx), &types.UserConcentratedSuperfluidPositionsUnbondingRequest{
DelegatorAddress: s.TestAccs[0].String(),
})
s.Require().NoError(err)

// The result should only have the four superfluid positions that were initially created
s.Require().Equal(4, len(res.ClPoolUserPositionRecords))
s.Require().Equal(4, len(expectedPositionIds))
s.Require().Equal(4, len(expectedLockIds))
// The result should only have the four unbonding superfluid positions
s.Require().Equal(4, len(unbondingRes.ClPoolUserPositionRecords))
s.Require().Equal(4, len(expectedUnbondingPositionIds))
s.Require().Equal(4, len(expectedUnbondingLockIds))

actualPositionIds := []uint64{}
actualLockIds := []uint64{}
actualTotalSharesLocked := sdk.Coins{}
for _, record := range res.ClPoolUserPositionRecords {
actualUnbondingPositionIds := []uint64{}
actualUnbondingLockIds := []uint64{}
actualUnbondingTotalSharesLocked := sdk.Coins{}
for _, record := range unbondingRes.ClPoolUserPositionRecords {
s.Require().Equal(record.ValidatorAddress, valAddrs[0].String()) // User 0 only used this validator
actualPositionIds = append(actualPositionIds, record.PositionId)
actualLockIds = append(actualLockIds, record.LockId)
actualTotalSharesLocked = actualTotalSharesLocked.Add(record.DelegationAmount)
actualUnbondingPositionIds = append(actualUnbondingPositionIds, record.PositionId)
actualUnbondingLockIds = append(actualUnbondingLockIds, record.LockId)
actualUnbondingTotalSharesLocked = actualUnbondingTotalSharesLocked.Add(record.DelegationAmount)
}

s.Require().True(osmoutils.ContainsDuplicateDeepEqual([]interface{}{expectedPositionIds, actualPositionIds}))
s.Require().True(osmoutils.ContainsDuplicateDeepEqual([]interface{}{expectedLockIds, actualLockIds}))
s.Require().Equal(expectedTotalSharesLocked, actualTotalSharesLocked)
s.Require().True(osmoutils.ContainsDuplicateDeepEqual([]interface{}{expectedUnbondingPositionIds, actualUnbondingPositionIds}))
s.Require().True(osmoutils.ContainsDuplicateDeepEqual([]interface{}{expectedUnbondingLockIds, actualUnbondingLockIds}))
s.Require().Equal(expectedUnbondingTotalSharesLocked, actualUnbondingTotalSharesLocked)

}

func (s *KeeperTestSuite) TestGRPCQueryTotalDelegationByDelegator() {
Expand Down
Loading