Skip to content

Commit

Permalink
feat: swap estimation by denom
Browse files Browse the repository at this point in the history
  • Loading branch information
cosmic-vagabond committed Nov 22, 2023
1 parent 85c850a commit 3e7d3f2
Show file tree
Hide file tree
Showing 30 changed files with 1,625 additions and 177 deletions.
19 changes: 19 additions & 0 deletions proto/elys/amm/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ service Query {
option (google.api.http).get = "/elys-network/elys/amm/out_route_by_denom/{denom_out}/{denom_in}";

}

// Queries a list of SwapEstimationByDenom items.
rpc SwapEstimationByDenom (QuerySwapEstimationByDenomRequest) returns (QuerySwapEstimationByDenomResponse) {
option (google.api.http).get = "/elys-network/elys/amm/swap_estimation_by_denom";

}
}
// QueryParamsRequest is request type for the Query/Params RPC method.
message QueryParamsRequest {}
Expand Down Expand Up @@ -175,3 +181,16 @@ message QueryOutRouteByDenomResponse {
repeated SwapAmountOutRoute out_route = 1;
}

message QuerySwapEstimationByDenomRequest {
cosmos.base.v1beta1.Coin amount = 1 [(gogoproto.nullable) = false];
string denom_in = 2;
string denom_out = 3;
}

message QuerySwapEstimationByDenomResponse {
repeated SwapAmountInRoute in_route = 1;
repeated SwapAmountOutRoute out_route = 2;
string spot_price = 3 [(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", (gogoproto.nullable) = false];
cosmos.base.v1beta1.Coin amount = 4 [(gogoproto.nullable) = false];
}

13 changes: 9 additions & 4 deletions testutil/keeper/amm.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import (
typesparams "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/elys-network/elys/x/amm/keeper"
"github.com/elys-network/elys/x/amm/types"
"github.com/elys-network/elys/x/amm/types/mocks"
"github.com/stretchr/testify/require"
)

func AmmKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) {
func AmmKeeper(t testing.TB) (*keeper.Keeper, sdk.Context, *mocks.AccountedPoolKeeper, *mocks.OracleKeeper) {
storeKey := sdk.NewKVStoreKey(types.StoreKey)
transientStoreKey := storetypes.NewTransientStoreKey(types.TStoreKey)

Expand All @@ -36,23 +37,27 @@ func AmmKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) {
transientStoreKey,
"AmmParams",
)

accountedPoolKeeper := mocks.NewAccountedPoolKeeper(t)
oracleKeeper := mocks.NewOracleKeeper(t)

k := keeper.NewKeeper(
cdc,
storeKey,
transientStoreKey,
paramsSubspace,
nil,
nil,
oracleKeeper,
nil,
nil,
nil,
nil,
accountedPoolKeeper,
)

ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger())

// Initialize params
k.SetParams(ctx, types.DefaultParams())

return k, ctx
return k, ctx, accountedPoolKeeper, oracleKeeper
}
23 changes: 12 additions & 11 deletions wasmbindings/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,18 @@ type ElysQuery struct {
AccountedPoolAccountedPoolAll *accountedpooltypes.QueryAllAccountedPoolRequest `json:"accounted_pool_accounted_pool_all,omitempty"`

// amm queriers
AmmParams *ammtypes.QueryParamsRequest `json:"amm_params,omitempty"`
AmmPool *ammtypes.QueryGetPoolRequest `json:"amm_pool,omitempty"`
AmmPoolAll *ammtypes.QueryAllPoolRequest `json:"amm_pool_all,omitempty"`
AmmDenomLiquidity *ammtypes.QueryGetDenomLiquidityRequest `json:"amm_denom_liquidity,omitempty"`
AmmDenomLiquidityAll *ammtypes.QueryAllDenomLiquidityRequest `json:"amm_denom_liquidity_all,omitempty"`
AmmSwapEstimation *ammtypes.QuerySwapEstimationRequest `json:"amm_swap_estimation,omitempty"`
AmmSlippageTrack *ammtypes.QuerySlippageTrackRequest `json:"amm_slippage_track,omitempty"`
AmmSlippageTrackAll *ammtypes.QuerySlippageTrackAllRequest `json:"amm_slippage_track_all,omitempty"`
AmmBalance *ammtypes.QueryBalanceRequest `json:"amm_balance,omitempty"`
AmmInRouteByDenom *ammtypes.QueryInRouteByDenomRequest `json:"amm_in_route_by_denom,omitempty"`
AmmOutRouteByDenom *ammtypes.QueryOutRouteByDenomRequest `json:"amm_out_route_by_denom,omitempty"`
AmmParams *ammtypes.QueryParamsRequest `json:"amm_params,omitempty"`
AmmPool *ammtypes.QueryGetPoolRequest `json:"amm_pool,omitempty"`
AmmPoolAll *ammtypes.QueryAllPoolRequest `json:"amm_pool_all,omitempty"`
AmmDenomLiquidity *ammtypes.QueryGetDenomLiquidityRequest `json:"amm_denom_liquidity,omitempty"`
AmmDenomLiquidityAll *ammtypes.QueryAllDenomLiquidityRequest `json:"amm_denom_liquidity_all,omitempty"`
AmmSwapEstimation *ammtypes.QuerySwapEstimationRequest `json:"amm_swap_estimation,omitempty"`
AmmSwapEstimationByDenom *ammtypes.QuerySwapEstimationByDenomRequest `json:"amm_swap_estimation_by_denom,omitempty"`
AmmSlippageTrack *ammtypes.QuerySlippageTrackRequest `json:"amm_slippage_track,omitempty"`
AmmSlippageTrackAll *ammtypes.QuerySlippageTrackAllRequest `json:"amm_slippage_track_all,omitempty"`
AmmBalance *ammtypes.QueryBalanceRequest `json:"amm_balance,omitempty"`
AmmInRouteByDenom *ammtypes.QueryInRouteByDenomRequest `json:"amm_in_route_by_denom,omitempty"`
AmmOutRouteByDenom *ammtypes.QueryOutRouteByDenomRequest `json:"amm_out_route_by_denom,omitempty"`

// assetprofile queriers
AssetProfileParams *assetprofiletypes.QueryParamsRequest `json:"asset_profile_params,omitempty"`
Expand Down
4 changes: 3 additions & 1 deletion x/amm/client/cli/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ func GetQueryCmd(queryRoute string) *cobra.Command {

cmd.AddCommand(CmdOutRouteByDenom())

// this line is used by starport scaffolding # 1
cmd.AddCommand(CmdSwapEstimationByDenom())

// this line is used by starport scaffolding # 1

return cmd
}
55 changes: 55 additions & 0 deletions x/amm/client/cli/query_swap_estimation_by_denom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cli

import (
"strconv"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/elys-network/elys/x/amm/types"
"github.com/spf13/cobra"
)

var _ = strconv.Itoa(0)

func CmdSwapEstimationByDenom() *cobra.Command {
cmd := &cobra.Command{
Use: "swap-estimation-by-denom [amount] [denom-in] [denom-out]",
Short: "Query swap-estimation-by-denom",
Example: "elysd q amm swap-estimation-by-denom 100uatom uatom uosmo",
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) (err error) {
reqAmount, err := sdk.ParseCoinNormalized(args[0])
if err != nil {
return err
}
reqDenomIn := args[1]
reqDenomOut := args[2]

clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

queryClient := types.NewQueryClient(clientCtx)

params := &types.QuerySwapEstimationByDenomRequest{

Amount: reqAmount,
DenomIn: reqDenomIn,
DenomOut: reqDenomOut,
}

res, err := queryClient.SwapEstimationByDenom(cmd.Context(), params)
if err != nil {
return err
}

return clientCtx.PrintProto(res)
},
}

flags.AddQueryFlagsToCmd(cmd)

return cmd
}
2 changes: 2 additions & 0 deletions x/amm/client/wasm/querier.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func (oq *Querier) HandleQuery(ctx sdk.Context, query wasmbindingstypes.ElysQuer
return oq.queryDenomLiquidityAll(ctx, query.AmmDenomLiquidityAll)
case query.AmmSwapEstimation != nil:
return oq.querySwapEstimation(ctx, query.AmmSwapEstimation)
case query.AmmSwapEstimationByDenom != nil:
return oq.querySwapEstimationByDenom(ctx, query.AmmSwapEstimationByDenom)
case query.AmmSlippageTrack != nil:
return oq.querySlippageTrack(ctx, query.AmmSlippageTrack)
case query.AmmSlippageTrackAll != nil:
Expand Down
22 changes: 22 additions & 0 deletions x/amm/client/wasm/query_swap_estimation_by_denom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package wasm

import (
"encoding/json"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
ammtypes "github.com/elys-network/elys/x/amm/types"
)

func (oq *Querier) querySwapEstimationByDenom(ctx sdk.Context, query *ammtypes.QuerySwapEstimationByDenomRequest) ([]byte, error) {
res, err := oq.keeper.SwapEstimationByDenom(sdk.WrapSDKContext(ctx), query)
if err != nil {
return nil, errorsmod.Wrap(err, "failed to get swap estimation by denom")
}

responseBytes, err := json.Marshal(res)
if err != nil {
return nil, errorsmod.Wrap(err, "failed to serialize swap estimation by denom response")
}
return responseBytes, nil
}
2 changes: 1 addition & 1 deletion x/amm/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func TestGenesis(t *testing.T) {
// this line is used by starport scaffolding # genesis/test/state
}

k, ctx := keepertest.AmmKeeper(t)
k, ctx, _, _ := keepertest.AmmKeeper(t)
amm.InitGenesis(ctx, *k, genesisState)
got := amm.ExportGenesis(ctx, *k)
require.NotNil(t, got)
Expand Down
38 changes: 2 additions & 36 deletions x/amm/keeper/calc_in_route_by_denom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@ package keeper_test
import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
keepertest "github.com/elys-network/elys/testutil/keeper"
"github.com/elys-network/elys/x/amm/keeper"
"github.com/elys-network/elys/x/amm/types"
"github.com/stretchr/testify/require"
)

func TestCalcInRouteByDenom(t *testing.T) {
k, ctx := keepertest.AmmKeeper(t)
k, ctx, _, _ := keepertest.AmmKeeper(t)

// Setup mock pools and assets
setupMockPools(k, ctx)
SetupMockPools(k, ctx)

// Test direct pool route
route, err := k.CalcInRouteByDenom(ctx, "denom1", "denom2", "baseCurrency")
Expand All @@ -37,34 +34,3 @@ func TestCalcInRouteByDenom(t *testing.T) {
_, err = k.CalcInRouteByDenom(ctx, "denom1", "denom1", "baseCurrency")
require.Error(t, err)
}

func setupMockPools(k *keeper.Keeper, ctx sdk.Context) {
// Create and set mock pools
pools := []types.Pool{
{
PoolId: 1,
PoolAssets: []types.PoolAsset{
{Token: sdk.Coin{Denom: "denom1", Amount: sdk.NewInt(1000)}},
{Token: sdk.Coin{Denom: "denom2", Amount: sdk.NewInt(1000)}},
},
},
{
PoolId: 2,
PoolAssets: []types.PoolAsset{
{Token: sdk.Coin{Denom: "denom1", Amount: sdk.NewInt(1000)}},
{Token: sdk.Coin{Denom: "baseCurrency", Amount: sdk.NewInt(1000)}},
},
},
{
PoolId: 3,
PoolAssets: []types.PoolAsset{
{Token: sdk.Coin{Denom: "baseCurrency", Amount: sdk.NewInt(1000)}},
{Token: sdk.Coin{Denom: "denom3", Amount: sdk.NewInt(1000)}},
},
},
}

for _, pool := range pools {
k.SetPool(ctx, pool)
}
}
55 changes: 55 additions & 0 deletions x/amm/keeper/calc_in_route_spot_price.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/elys-network/elys/x/amm/types"
)

// CalcInRouteSpotPrice calculates the spot price of the given token and in route
func (k Keeper) CalcInRouteSpotPrice(ctx sdk.Context, tokenIn sdk.Coin, routes []*types.SwapAmountInRoute) (sdk.Dec, sdk.Coin, error) {
if routes == nil || len(routes) == 0 {
return sdk.ZeroDec(), sdk.Coin{}, types.ErrEmptyRoutes
}

// Start with the initial token input
tokensIn := sdk.Coins{tokenIn}

// The final token out denom
var tokenOutDenom string

for _, route := range routes {
poolId := route.PoolId
tokenOutDenom = route.TokenOutDenom

pool, found := k.GetPool(ctx, poolId)
if !found {
return sdk.ZeroDec(), sdk.Coin{}, types.ErrPoolNotFound
}

// Estimate swap
snapshot := k.GetPoolSnapshotOrSet(ctx, pool)
swapResult, err := k.CalcOutAmtGivenIn(ctx, pool.PoolId, k.oracleKeeper, &snapshot, tokensIn, tokenOutDenom, sdk.ZeroDec())

if err != nil {
return sdk.ZeroDec(), sdk.Coin{}, err
}

if swapResult.IsZero() {
return sdk.ZeroDec(), sdk.Coin{}, types.ErrAmountTooLow
}

// Use the current swap result as the input for the next iteration
tokensIn = sdk.Coins{swapResult}
}

// Calculate the spot price given the initial token in and the final token out
spotPrice := sdk.NewDecFromInt(tokensIn[0].Amount).Quo(sdk.NewDecFromInt(tokenIn.Amount))

// Calculate the token out amount
tokenOutAmt := sdk.NewDecFromInt(tokenIn.Amount).Mul(spotPrice)

// Construct the token out coin
tokenOut := sdk.NewCoin(tokenOutDenom, tokenOutAmt.TruncateInt())

return spotPrice, tokenOut, nil
}
50 changes: 50 additions & 0 deletions x/amm/keeper/calc_in_route_spot_price_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package keeper_test

import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
keepertest "github.com/elys-network/elys/testutil/keeper"
"github.com/elys-network/elys/x/amm/types"
"github.com/stretchr/testify/require"
)

func TestCalcInRouteSpotPrice(t *testing.T) {
k, ctx, accountedPoolKeeper, _ := keepertest.AmmKeeper(t)
SetupMockPools(k, ctx)

// Use token in for all tests
tokenIn := sdk.NewCoin("denom1", sdk.NewInt(100))

// Test single route
accountedPoolKeeper.On("GetAccountedBalance", ctx, uint64(1), "denom1").Return(sdk.NewInt(1000))
accountedPoolKeeper.On("GetAccountedBalance", ctx, uint64(1), "denom2").Return(sdk.NewInt(1000))
routes := []*types.SwapAmountInRoute{{PoolId: 1, TokenOutDenom: "denom2"}}
spotPrice, _, err := k.CalcInRouteSpotPrice(ctx, tokenIn, routes)
require.NoError(t, err)
require.NotZero(t, spotPrice)
accountedPoolKeeper.AssertExpectations(t)

// Test multiple routes
accountedPoolKeeper.On("GetAccountedBalance", ctx, uint64(2), "denom1").Return(sdk.NewInt(1000))
accountedPoolKeeper.On("GetAccountedBalance", ctx, uint64(2), "baseCurrency").Return(sdk.NewInt(1000))
accountedPoolKeeper.On("GetAccountedBalance", ctx, uint64(3), "baseCurrency").Return(sdk.NewInt(1000))
accountedPoolKeeper.On("GetAccountedBalance", ctx, uint64(3), "denom3").Return(sdk.NewInt(1000))
routes = []*types.SwapAmountInRoute{
{PoolId: 2, TokenOutDenom: "baseCurrency"},
{PoolId: 3, TokenOutDenom: "denom3"},
}
spotPrice, _, err = k.CalcInRouteSpotPrice(ctx, tokenIn, routes)
require.NoError(t, err)
require.NotZero(t, spotPrice)
accountedPoolKeeper.AssertExpectations(t)

// Test no routes
_, _, err = k.CalcInRouteSpotPrice(ctx, tokenIn, nil)
require.Error(t, err)

// Test invalid pool
routes = []*types.SwapAmountInRoute{{PoolId: 9999, TokenOutDenom: "denom2"}}
_, _, err = k.CalcInRouteSpotPrice(ctx, tokenIn, routes)
require.Error(t, err)
}
4 changes: 2 additions & 2 deletions x/amm/keeper/calc_out_route_by_denom_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import (
)

func TestCalcOutRouteByDenom(t *testing.T) {
k, ctx := keepertest.AmmKeeper(t)
k, ctx, _, _ := keepertest.AmmKeeper(t)

// Setup mock pools and assets
setupMockPools(k, ctx)
SetupMockPools(k, ctx)

// Test direct pool route
route, err := k.CalcOutRouteByDenom(ctx, "denom2", "denom1", "baseCurrency")
Expand Down
Loading

0 comments on commit 3e7d3f2

Please sign in to comment.