From 02cd400b0f7665d9045356c62a113dd7ca83b70c Mon Sep 17 00:00:00 2001 From: roman Date: Thu, 9 Nov 2023 16:55:19 +0000 Subject: [PATCH] format results per FE requirements --- ingest/sqs/domain/pools.go | 5 +- ingest/sqs/domain/router.go | 11 ++ .../sqs/pools/ingester/redis/pool_ingester.go | 5 +- .../router/delivery/http/router_handler.go | 2 + .../router/usecase/candidate_routes_test.go | 7 ++ ingest/sqs/router/usecase/export_test.go | 1 + ingest/sqs/router/usecase/optimized_routes.go | 54 +++++++-- .../router/usecase/optimized_routes_test.go | 10 ++ ...routable_pool.go => routable_cfmm_pool.go} | 0 .../router/usecase/routable_result_pool.go | 104 ++++++++++++++++++ ingest/sqs/router/usecase/route.go | 34 +++++- ingest/sqs/router/usecase/route_test.go | 101 +++++++++++++++++ 12 files changed, 323 insertions(+), 11 deletions(-) rename ingest/sqs/router/usecase/{routable_pool.go => routable_cfmm_pool.go} (100%) create mode 100644 ingest/sqs/router/usecase/routable_result_pool.go create mode 100644 ingest/sqs/router/usecase/route_test.go diff --git a/ingest/sqs/domain/pools.go b/ingest/sqs/domain/pools.go index 155df24900e..f5b1e364be2 100644 --- a/ingest/sqs/domain/pools.go +++ b/ingest/sqs/domain/pools.go @@ -43,8 +43,9 @@ type SQSPool struct { TotalValueLockedUSDC osmomath.Int `json:"total_value_locked_uosmo"` TotalValueLockedError string `json:"total_value_locked_error,omitempty"` // Only CL and Cosmwasm pools need balances appended - Balances sdk.Coins `json:"balances,string"` - PoolDenoms []string `json:"pool_denoms"` + Balances sdk.Coins `json:"balances,string"` + PoolDenoms []string `json:"pool_denoms"` + SpreadFactor osmomath.Dec `json:"spread_factor"` } type LiquidityDepthsWithRange = clqueryproto.LiquidityDepthWithRange diff --git a/ingest/sqs/domain/router.go b/ingest/sqs/domain/router.go index 6b5a60c4c86..beb662fc9fd 100644 --- a/ingest/sqs/domain/router.go +++ b/ingest/sqs/domain/router.go @@ -29,6 +29,13 @@ type Route interface { GetTokenOutDenom() string + // PrepareResultPools strips away unnecessary fields + // from each pool in the route, + // leaving only the data needed by client + // Note that it mutates the route. + // Returns the resulting pools. + PrepareResultPools() []RoutablePool + String() string } @@ -57,6 +64,10 @@ type Quote interface { GetAmountIn() sdk.Coin GetAmountOut() osmomath.Int GetRoute() []SplitRoute + + // PrepareResult mutates the quote to prepare + // it with the data formatted for output to the client. + PrepareResult() } type RouterConfig struct { diff --git a/ingest/sqs/pools/ingester/redis/pool_ingester.go b/ingest/sqs/pools/ingester/redis/pool_ingester.go index 6d897f0b0c2..1eacb146142 100644 --- a/ingest/sqs/pools/ingester/redis/pool_ingester.go +++ b/ingest/sqs/pools/ingester/redis/pool_ingester.go @@ -200,7 +200,9 @@ func (pi *poolIngester) convertPool( poolDenomsMap[poolDenom] = struct{}{} } - // Note that this must follow the call to GetPoolDenoms() + spreadFactor := pool.GetSpreadFactor(ctx) + + // Note that this must follow the call to GetPoolDenoms() and GetSpreadFactor. // Otherwise, the CosmWasmPool model panics. pool = pool.AsSerializablePool() @@ -311,6 +313,7 @@ func (pi *poolIngester) convertPool( TotalValueLockedError: errorInTVLStr, Balances: balances, PoolDenoms: denoms, + SpreadFactor: spreadFactor, }, TickModel: tickModel, }, nil diff --git a/ingest/sqs/router/delivery/http/router_handler.go b/ingest/sqs/router/delivery/http/router_handler.go index 0913cb86d58..cce72ec9bb3 100644 --- a/ingest/sqs/router/delivery/http/router_handler.go +++ b/ingest/sqs/router/delivery/http/router_handler.go @@ -53,6 +53,8 @@ func (a *RouterHandler) GetOptimalQuote(c echo.Context) error { return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()}) } + quote.PrepareResult() + return c.JSON(http.StatusOK, quote) } diff --git a/ingest/sqs/router/usecase/candidate_routes_test.go b/ingest/sqs/router/usecase/candidate_routes_test.go index 45a1fc1fa17..607afc467a6 100644 --- a/ingest/sqs/router/usecase/candidate_routes_test.go +++ b/ingest/sqs/router/usecase/candidate_routes_test.go @@ -22,6 +22,7 @@ type mockPool struct { poolType poolmanagertypes.PoolType tokenOutDenom string takerFee osmomath.Dec + spreadFactor osmomath.Dec } var ( @@ -39,6 +40,7 @@ func (mp *mockPool) GetSQSPoolModel() domain.SQSPool { return domain.SQSPool{ Balances: mp.Balances, TotalValueLockedUSDC: mp.totalValueLockedUSDC, + SpreadFactor: defaultSpreadFactor, } } @@ -108,6 +110,8 @@ func deepCopyPool(mp *mockPool) *mockPool { newTotalValueLocker := osmomath.NewIntFromBigInt(mp.totalValueLockedUSDC.BigInt()) + newBalances := sdk.NewCoins(mp.Balances...) + return &mockPool{ ID: mp.ID, denoms: newDenoms, @@ -117,6 +121,9 @@ func deepCopyPool(mp *mockPool) *mockPool { // Note these are not deep copied. ChainPoolModel: mp.ChainPoolModel, tokenOutDenom: mp.tokenOutDenom, + Balances: newBalances, + takerFee: mp.takerFee.Clone(), + spreadFactor: mp.spreadFactor.Clone(), } } diff --git a/ingest/sqs/router/usecase/export_test.go b/ingest/sqs/router/usecase/export_test.go index 59a34b9fd9d..b4a5314e185 100644 --- a/ingest/sqs/router/usecase/export_test.go +++ b/ingest/sqs/router/usecase/export_test.go @@ -11,6 +11,7 @@ type ( RoutableCFMMPoolImpl = routableCFMMPoolImpl RoutableConcentratedPoolImpl = routableConcentratedPoolImpl RoutableTransmuterPoolImpl = routableTransmuterPoolImpl + RoutableResultPoolImpl = routableResultPoolImpl ) const ( diff --git a/ingest/sqs/router/usecase/optimized_routes.go b/ingest/sqs/router/usecase/optimized_routes.go index da0370c2954..c0a3f74ab71 100644 --- a/ingest/sqs/router/usecase/optimized_routes.go +++ b/ingest/sqs/router/usecase/optimized_routes.go @@ -13,9 +13,45 @@ import ( ) type quoteImpl struct { - AmountIn sdk.Coin "json:\"amount_in\"" - AmountOut osmomath.Int "json:\"amount_out\"" - Route []domain.SplitRoute "json:\"route\"" + AmountIn sdk.Coin "json:\"amount_in\"" + AmountOut osmomath.Int "json:\"amount_out\"" + Route []domain.SplitRoute "json:\"route\"" + EffectiveSpreadFactor osmomath.Dec "json:\"effective_spread_factor\"" +} + +var _ domain.Quote = "eImpl{} + +// PrepareResult implements domain.Quote. +// PrepareResult mutates the quote to prepare +// it with the data formatted for output to the client. +// Specifically: +// It strips away unnecessary fields from each pool in the route. +// Computes an effective spread factor from all routes. +func (q *quoteImpl) PrepareResult() { + + totalAmountIn := q.AmountIn.Amount.ToLegacyDec() + totalSpreadFactorAcrossRoutes := osmomath.ZeroDec() + + for _, route := range q.Route { + + routeSpreadFactor := osmomath.ZeroDec() + routeAmountInFraction := route.GetAmountIn().ToLegacyDec().Quo(totalAmountIn) + + // Calculate the spread factor across pools in the route + for _, pool := range route.GetPools() { + spreadFactor := pool.GetSQSPoolModel().SpreadFactor + + routeSpreadFactor = routeSpreadFactor.AddMut( + osmomath.OneDec().SubMut(routeSpreadFactor).MulTruncateMut(spreadFactor), + ) + } + + totalSpreadFactorAcrossRoutes = totalSpreadFactorAcrossRoutes.AddMut(routeSpreadFactor.MulMut(routeAmountInFraction)) + + route.PrepareResultPools() + } + + q.EffectiveSpreadFactor = totalSpreadFactorAcrossRoutes } // GetAmountIn implements Quote. @@ -126,11 +162,13 @@ func (r *Router) estimateBestSingleRouteQuote(routes []domain.Route, tokenIn sdk return nil, errors.New("did not find a working direct route") } - return "eImpl{ + finalQuote := "eImpl{ AmountIn: tokenIn, AmountOut: bestRoute.OutAmount, Route: []domain.SplitRoute{bestRoute}, - }, nil + } + + return finalQuote, nil } // CONTRACT: all routes are valid. Must be validated by the caller with validateRoutes method. @@ -150,11 +188,13 @@ func (r *Router) estimateBestSplitRouteQuote(routes []domain.Route, tokenIn sdk. r.logger.Debug("bestSplit", zap.Any("value", bestSplit)) - return "eImpl{ + finalQuote := "eImpl{ AmountIn: tokenIn, AmountOut: bestSplit.CurrentTotalOut, Route: bestSplit.Routes, - }, nil + } + + return finalQuote, nil } // validateAndFilterRoutes validates all routes. Specifically: diff --git a/ingest/sqs/router/usecase/optimized_routes_test.go b/ingest/sqs/router/usecase/optimized_routes_test.go index f1de92040fb..e9499ed81ea 100644 --- a/ingest/sqs/router/usecase/optimized_routes_test.go +++ b/ingest/sqs/router/usecase/optimized_routes_test.go @@ -18,11 +18,21 @@ const defaultPoolID = uint64(1) // TODO: copy exists in candidate_routes_test.go - share & reuse var ( + defaultTakerFee = osmomath.MustNewDecFromStr("0.002") + defaultPoolBalances = sdk.NewCoins( + sdk.NewCoin(denomOne, DefaultAmt0), + sdk.NewCoin(denomTwo, DefaultAmt1), + ) + defaultSpreadFactor = osmomath.MustNewDecFromStr("0.005") + defaultPool = &mockPool{ ID: defaultPoolID, denoms: []string{denomOne, denomTwo}, totalValueLockedUSDC: osmomath.NewInt(10), poolType: poolmanagertypes.Balancer, + Balances: defaultPoolBalances, + takerFee: defaultTakerFee, + spreadFactor: defaultSpreadFactor, } emptyRoute = &routerusecase.RouteImpl{} diff --git a/ingest/sqs/router/usecase/routable_pool.go b/ingest/sqs/router/usecase/routable_cfmm_pool.go similarity index 100% rename from ingest/sqs/router/usecase/routable_pool.go rename to ingest/sqs/router/usecase/routable_cfmm_pool.go diff --git a/ingest/sqs/router/usecase/routable_result_pool.go b/ingest/sqs/router/usecase/routable_result_pool.go new file mode 100644 index 00000000000..1d89bd72468 --- /dev/null +++ b/ingest/sqs/router/usecase/routable_result_pool.go @@ -0,0 +1,104 @@ +package usecase + +import ( + "errors" + "fmt" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/osmosis-labs/osmosis/osmomath" + "github.com/osmosis-labs/osmosis/v20/ingest/sqs/domain" + "github.com/osmosis-labs/osmosis/v20/x/poolmanager" + + poolmanagertypes "github.com/osmosis-labs/osmosis/v20/x/poolmanager/types" +) + +var _ domain.RoutablePool = &routableCFMMPoolImpl{} + +// routableResultPoolImpl is a generalized implementation that is returned to the client +// side in quotes. It contains all the relevant pool data needed for Osmosis frontend +type routableResultPoolImpl struct { + ID uint64 "json:\"id\"" + Type poolmanagertypes.PoolType "json:\"type\"" + Balances sdk.Coins "json:\"balances\"" + SpreadFactor osmomath.Dec "json:\"spread_factor\"" + TokenOutDenom string "json:\"token_out_denom\"" + TakerFee osmomath.Dec "json:\"taker_fee\"" +} + +// GetId implements domain.RoutablePool. +func (r *routableResultPoolImpl) GetId() uint64 { + return r.ID +} + +// GetPoolDenoms implements domain.RoutablePool. +func (r *routableResultPoolImpl) GetPoolDenoms() []string { + denoms := make([]string, len(r.Balances)) + for _, balance := range r.Balances { + denoms = append(denoms, balance.Denom) + } + + return denoms +} + +// GetSQSPoolModel implements domain.RoutablePool. +func (r *routableResultPoolImpl) GetSQSPoolModel() domain.SQSPool { + return domain.SQSPool{ + Balances: r.Balances, + PoolDenoms: r.GetPoolDenoms(), + SpreadFactor: r.SpreadFactor, + } +} + +// GetTickModel implements domain.RoutablePool. +func (r *routableResultPoolImpl) GetTickModel() (*domain.TickModel, error) { + return nil, errors.New("not implemented") +} + +// GetTotalValueLockedUOSMO implements domain.RoutablePool. +func (*routableResultPoolImpl) GetTotalValueLockedUOSMO() math.Int { + return osmomath.Int{} +} + +// GetType implements domain.RoutablePool. +func (r *routableResultPoolImpl) GetType() poolmanagertypes.PoolType { + return r.Type +} + +// GetUnderlyingPool implements domain.RoutablePool. +func (*routableResultPoolImpl) GetUnderlyingPool() poolmanagertypes.PoolI { + return nil +} + +// Validate implements domain.RoutablePool. +func (*routableResultPoolImpl) Validate(minUOSMOTVL math.Int) error { + return nil +} + +// CalculateTokenOutByTokenIn implements RoutablePool. +func (r *routableResultPoolImpl) CalculateTokenOutByTokenIn(tokenIn sdk.Coin) (sdk.Coin, error) { + return sdk.Coin{}, errors.New("not implemented") +} + +// GetTokenOutDenom implements RoutablePool. +func (rp *routableResultPoolImpl) GetTokenOutDenom() string { + return rp.TokenOutDenom +} + +// String implements domain.RoutablePool. +func (r *routableResultPoolImpl) String() string { + return fmt.Sprintf("pool (%d), pool type (%d), pool denoms (%v)", r.GetId(), r.GetType(), r.GetPoolDenoms()) +} + +// ChargeTakerFee implements domain.RoutablePool. +// Charges the taker fee for the given token in and returns the token in after the fee has been charged. +func (r *routableResultPoolImpl) ChargeTakerFeeExactIn(tokenIn sdk.Coin) (tokenInAfterFee sdk.Coin) { + tokenInAfterTakerFee, _ := poolmanager.CalcTakerFeeExactIn(tokenIn, r.TakerFee) + return tokenInAfterTakerFee +} + +// GetTakerFee implements domain.RoutablePool. +func (r *routableResultPoolImpl) GetTakerFee() math.LegacyDec { + return r.TakerFee +} diff --git a/ingest/sqs/router/usecase/route.go b/ingest/sqs/router/usecase/route.go index 0d6aeee4800..cf1246cb46f 100644 --- a/ingest/sqs/router/usecase/route.go +++ b/ingest/sqs/router/usecase/route.go @@ -13,7 +13,39 @@ import ( var _ domain.Route = &routeImpl{} type routeImpl struct { - Pools []domain.RoutablePool + Pools []domain.RoutablePool "json:\"pools\"" +} + +// PrepareResultPools implements domain.Route. +// Strips away unnecessary fields from each pool in the route, +// leaving only the data needed by client +// The following are the list of fields that are returned to the client in each pool: +// - ID +// - Type +// - Balances +// - Spread Factor +// - Token Out Denom +// - Taker Fee +// Note that it mutates the route. +// Returns the resulting pools. +func (r *routeImpl) PrepareResultPools() []domain.RoutablePool { + for i, pool := range r.Pools { + + sqsModel := pool.GetSQSPoolModel() + + r.Pools[i] = &routableResultPoolImpl{ + ID: pool.GetId(), + Type: pool.GetType(), + Balances: sqsModel.Balances, + // Note that we cannot get the SpreadFactor method on + // the CosmWasm pool models as it does not implement it. + // As a result, we propagate it via SQS model. + SpreadFactor: sqsModel.SpreadFactor, + TokenOutDenom: pool.GetTokenOutDenom(), + TakerFee: pool.GetTakerFee(), + } + } + return r.Pools } // GetPools implements Route. diff --git a/ingest/sqs/router/usecase/route_test.go b/ingest/sqs/router/usecase/route_test.go new file mode 100644 index 00000000000..2bb06bcb962 --- /dev/null +++ b/ingest/sqs/router/usecase/route_test.go @@ -0,0 +1,101 @@ +package usecase_test + +import ( + "github.com/osmosis-labs/osmosis/v20/ingest/sqs/domain" + "github.com/osmosis-labs/osmosis/v20/ingest/sqs/router/usecase" + poolmanagertypes "github.com/osmosis-labs/osmosis/v20/x/poolmanager/types" +) + +// This test validates that the pools in the route are converted into a new serializable +// type for clients with the following list of fields that are returned in each pool: +// - ID +// - Type +// - Balances +// - Spread Factor +// - Token Out Denom +// - Taker Fee +func (s *RouterTestSuite) TestPrepareResultPools() { + s.Setup() + + balancerPoolID := s.PrepareBalancerPool() + + balancerPool, err := s.App.PoolManagerKeeper.GetPool(s.Ctx, balancerPoolID) + s.Require().NoError(err) + + testcases := map[string]struct { + route domain.Route + + expectedPools []domain.RoutablePool + }{ + "empty route": { + route: emptyRoute.DeepCopy(), + + expectedPools: []domain.RoutablePool{}, + }, + "single balancer pool in route": { + route: withRoutePools( + emptyRoute, + []domain.RoutablePool{ + withChainPoolModel(withTokenOutDenom(defaultPool, denomOne), balancerPool), + }, + ), + + expectedPools: []domain.RoutablePool{ + &usecase.RoutableResultPoolImpl{ + ID: balancerPoolID, + Type: poolmanagertypes.Balancer, + Balances: defaultPoolBalances, + SpreadFactor: defaultSpreadFactor, + TokenOutDenom: denomOne, + TakerFee: defaultTakerFee, + }, + }, + }, + + // TODO: + // add tests with more pool types as well as multiple pools in the route + // https://app.clickup.com/t/86a1cfwag + } + + for name, tc := range testcases { + tc := tc + s.Run(name, func() { + + resultPools := tc.route.PrepareResultPools() + + s.validateRoutePools(tc.expectedPools, resultPools) + s.validateRoutePools(tc.expectedPools, tc.route.GetPools()) + }) + } +} + +// validateRoutePools validates that the expected pools are equal to the actual pools. +// Specifically, validates the following fields: +// - ID +// - Type +// - Balances +// - Spread Factor +// - Token Out Denom +// - Taker Fee +func (s *RouterTestSuite) validateRoutePools(expectedPools []domain.RoutablePool, actualPools []domain.RoutablePool) { + + s.Require().Equal(len(expectedPools), len(actualPools)) + + for i, expectedPool := range expectedPools { + actualPool := actualPools[i] + + expectedResultPool, ok := expectedPool.(*usecase.RoutableResultPoolImpl) + s.Require().True(ok) + + // Cast to result pool + actualResultPool, ok := actualPool.(*usecase.RoutableResultPoolImpl) + s.Require().True(ok) + + s.Require().Equal(expectedResultPool.ID, actualResultPool.ID) + s.Require().Equal(expectedResultPool.Type, actualResultPool.Type) + s.Require().Equal(expectedResultPool.Balances.String(), actualResultPool.Balances.String()) + s.Require().Equal(expectedResultPool.SpreadFactor.String(), actualResultPool.SpreadFactor.String()) + s.Require().Equal(expectedResultPool.TokenOutDenom, actualResultPool.TokenOutDenom) + s.Require().Equal(expectedResultPool.TakerFee.String(), actualResultPool.TakerFee.String()) + } +}