Skip to content

Commit

Permalink
chore: sf undelegating / delegated cl queries (#5691)
Browse files Browse the repository at this point in the history
* undelegating / delegated cl queries

* correct comment

* remove switch and add errors.Is

* Update proto/osmosis/superfluid/query.proto

Co-authored-by: Jon Ator <[email protected]>

* Update proto/osmosis/superfluid/query.proto

Co-authored-by: Jon Ator <[email protected]>

* rename to delegate in place of bond

* fix errors Is

---------

Co-authored-by: Jon Ator <[email protected]>
  • Loading branch information
czarcas7ic and jonator authored Jun 30, 2023
1 parent 4dafea2 commit 11d77c3
Show file tree
Hide file tree
Showing 5 changed files with 854 additions and 318 deletions.
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 UserConcentratedSuperfluidPositionsDelegated(
UserConcentratedSuperfluidPositionsDelegatedRequest)
returns (UserConcentratedSuperfluidPositionsDelegatedResponse) {
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 UserConcentratedSuperfluidPositionsUndelegating(
UserConcentratedSuperfluidPositionsUndelegatingRequest)
returns (UserConcentratedSuperfluidPositionsUndelegatingResponse) {
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 UserConcentratedSuperfluidPositionsDelegatedRequest {
string delegator_address = 1;
}

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

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

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

import (
"context"
"errors"
"fmt"
"strings"
"time"
Expand All @@ -19,6 +20,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 +282,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) {
// UserConcentratedSuperfluidPositionsDelegated returns all the cl superfluid positions for the specified delegator across all concentrated pools that are bonded.
func (q Querier) UserConcentratedSuperfluidPositionsDelegated(goCtx context.Context, req *types.UserConcentratedSuperfluidPositionsDelegatedRequest) (*types.UserConcentratedSuperfluidPositionsDelegatedResponse, 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.UserConcentratedSuperfluidPositionsDelegatedResponse{
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
}
// UserConcentratedSuperfluidPositionsUndelegating returns all the cl superfluid positions for the specified delegator across all concentrated pools that are unbonding.
func (q Querier) UserConcentratedSuperfluidPositionsUndelegating(goCtx context.Context, req *types.UserConcentratedSuperfluidPositionsUndelegatingRequest) (*types.UserConcentratedSuperfluidPositionsUndelegatingResponse, 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.UserConcentratedSuperfluidPositionsUndelegatingResponse{
ClPoolUserPositionRecords: clPoolUserPositionRecords,
}, nil
}
Expand Down Expand Up @@ -653,3 +634,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{PositionId: pos.PositionId}) {
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, errorsmod.Wrap(lockuptypes.ErrLockupNotFound, fmt.Sprintf("lock with ID %d does not exist", lock.GetID()))) {
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.UserConcentratedSuperfluidPositionsDelegated(sdk.WrapSDKContext(s.Ctx), &types.UserConcentratedSuperfluidPositionsDelegatedRequest{
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.UserConcentratedSuperfluidPositionsUndelegating(sdk.WrapSDKContext(s.Ctx), &types.UserConcentratedSuperfluidPositionsUndelegatingRequest{
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

0 comments on commit 11d77c3

Please sign in to comment.