From b0aee0006ce55d0851773084bd7880db7e32ad70 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:34:16 -0700 Subject: [PATCH] refactor/test: prepare for ranked route caching; clean up mainnet tests (#7087) (#7092) * refactor/test: prepare for ranked route caching; clean up mainnet tests * Update ingest/sqs/router/usecase/router_usecase.go * Update ingest/sqs/router/usecase/router_usecase.go * lint (cherry picked from commit acb26bd590dc521dc4d104ad40941751acbffc20) Co-authored-by: Roman --- .vscode/launch.json | 2 +- ingest/sqs/router/README.md | 4 +- ingest/sqs/router/usecase/export_test.go | 6 +- ingest/sqs/router/usecase/optimized_routes.go | 69 ----- .../router/usecase/optimized_routes_test.go | 252 ++++++++---------- ingest/sqs/router/usecase/router_usecase.go | 144 +++++++++- 6 files changed, 248 insertions(+), 229 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 37ab58d7289..437578dee06 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -318,7 +318,7 @@ "-test.timeout", "30m", "-test.run", - "TestRouterTestSuite/TestGetCustomQuote_Mainnet_UOSMOUION", + "TestRouterTestSuite/TestGetOptimalQuote", "-test.v" ], }, diff --git a/ingest/sqs/router/README.md b/ingest/sqs/router/README.md index c17fcbc9d92..4a2127cc4f7 100644 --- a/ingest/sqs/router/README.md +++ b/ingest/sqs/router/README.md @@ -6,13 +6,13 @@ ### Quote ```bash -curl "localhost:9092/quote?tokenIn=5000000uosmo&tokenOutDenom=uusdc" | jq . +curl "localhost:9092/router/quote?tokenIn=5000000uosmo&tokenOutDenom=uion" | jq . ``` ### Pools ```bash -curl "localhost:9092/all-pools" | jq . +curl "localhost:9092/pools/all" | jq . ``` ## Trade-offs To Re-evaluate diff --git a/ingest/sqs/router/usecase/export_test.go b/ingest/sqs/router/usecase/export_test.go index 828a6b11744..bc94311e0fa 100644 --- a/ingest/sqs/router/usecase/export_test.go +++ b/ingest/sqs/router/usecase/export_test.go @@ -31,11 +31,7 @@ func (r *routerUseCaseImpl) InitializeRouter() *Router { } func (r *routerUseCaseImpl) HandleRoutes(ctx context.Context, router *Router, tokenInDenom, tokenOutDenom string) (candidateRoutes route.CandidateRoutes, err error) { - return r.handleRoutes(ctx, router, tokenInDenom, tokenOutDenom) -} - -func (r *Router) GetOptimalQuote(tokenIn sdk.Coin, routes []route.RouteImpl) (domain.Quote, error) { - return r.getOptimalQuote(tokenIn, routes) + return r.handleCandidateRoutes(ctx, router, tokenInDenom, tokenOutDenom) } func (r *Router) EstimateBestSingleRouteQuote(routes []route.RouteImpl, tokenIn sdk.Coin) (domain.Quote, []RouteWithOutAmount, error) { diff --git a/ingest/sqs/router/usecase/optimized_routes.go b/ingest/sqs/router/usecase/optimized_routes.go index c46bc85e928..18c08b85000 100644 --- a/ingest/sqs/router/usecase/optimized_routes.go +++ b/ingest/sqs/router/usecase/optimized_routes.go @@ -13,73 +13,6 @@ import ( "github.com/osmosis-labs/osmosis/v21/ingest/sqs/router/usecase/route" ) -// getOptimalQuote returns the optimal quote by estimating the optimal route(s) through pools -// Considers all routes and splits. -// Returns error if router repository is not set on the router. -func (r *Router) getOptimalQuote(tokenIn sdk.Coin, routes []route.RouteImpl) (domain.Quote, error) { - if r.routerRepository == nil { - return nil, ErrNilRouterRepository - } - if r.poolsUsecase == nil { - return nil, ErrNilPoolsRepository - } - - bestSingleRouteQuote, routesSortedByAmtOut, err := r.estimateBestSingleRouteQuote(routes, tokenIn) - if err != nil { - return nil, err - } - - if r.maxSplitRoutes == 0 { - return bestSingleRouteQuote, nil - } - - numRoutes := len(routesSortedByAmtOut) - - // If there are more routes than the max split routes, keep only the top routes - if len(routesSortedByAmtOut) > r.maxSplitRoutes { - // Keep only top routes for splits - routes = routes[:r.maxSplitRoutes] - numRoutes = r.maxSplitRoutes - } - - // Convert routes sorted by amount out to routes - for i := 0; i < numRoutes; i++ { - // Update routes with the top routes - routes[i] = routesSortedByAmtOut[i].RouteImpl - } - - r.logger.Info("bestSingleRouteQuote ", zap.Stringer("quote", bestSingleRouteQuote)) - - bestSplitRouteQuote, err := r.GetSplitQuote(routes, tokenIn) - if err != nil { - return nil, err - } - - r.logger.Info("bestSplitRouteQuote ", zap.Any("out", bestSingleRouteQuote.GetAmountOut())) - - finalQuote := bestSingleRouteQuote - - // If the split route quote is better than the single route quote, return the split route quote - if bestSplitRouteQuote.GetAmountOut().GT(bestSingleRouteQuote.GetAmountOut()) { - routes := bestSplitRouteQuote.GetRoute() - - r.logger.Debug("split route selected", zap.Int("route_count", len(routes))) - for _, route := range routes { - r.logger.Debug("route", zap.Stringer("route", route)) - } - - finalQuote = bestSplitRouteQuote - } - - r.logger.Debug("single route selected", zap.Stringer("route", bestSingleRouteQuote.GetRoute()[0])) - - if finalQuote.GetAmountOut().IsZero() { - return nil, errors.New("best we can do is no tokens out") - } - - return finalQuote, nil -} - // getSingleRouteQuote returns the best single route quote for the given tokenIn and tokenOutDenom. // Returns error if router repository is not set on the router. func (r *Router) getBestSingleRouteQuote(tokenIn sdk.Coin, routes []route.RouteImpl) (quote domain.Quote, err error) { @@ -95,8 +28,6 @@ func (r *Router) getBestSingleRouteQuote(tokenIn sdk.Coin, routes []route.RouteI return nil, err } - r.logger.Info("bestSingleRouteQuote ", zap.Any("out", bestSingleRouteQuote.GetAmountOut())) - return bestSingleRouteQuote, nil } diff --git a/ingest/sqs/router/usecase/optimized_routes_test.go b/ingest/sqs/router/usecase/optimized_routes_test.go index c8d521fba43..5953323587a 100644 --- a/ingest/sqs/router/usecase/optimized_routes_test.go +++ b/ingest/sqs/router/usecase/optimized_routes_test.go @@ -12,6 +12,7 @@ import ( "github.com/osmosis-labs/osmosis/osmoutils/osmoassert" "github.com/osmosis-labs/osmosis/v21/ingest/sqs/domain" "github.com/osmosis-labs/osmosis/v21/ingest/sqs/domain/mocks" + "github.com/osmosis-labs/osmosis/v21/ingest/sqs/domain/mvc" "github.com/osmosis-labs/osmosis/v21/ingest/sqs/log" poolsusecase "github.com/osmosis-labs/osmosis/v21/ingest/sqs/pools/usecase" "github.com/osmosis-labs/osmosis/v21/ingest/sqs/router/usecase" @@ -551,169 +552,130 @@ func (s *RouterTestSuite) TestValidateAndFilterRoutes() { } } -// Validates that USDT for UMEE quote does not cause an error. -// This pair originally caused an error due to the lack of filtering that was -// added later. -func (s *RouterTestSuite) TestGetBestSplitRoutesQuote_Mainnet_USDTUMEE() { - config := defaultRouterConfig - config.MaxPoolsPerRoute = 5 - config.MaxRoutes = 10 - - var ( - amountIn = osmomath.NewInt(1000_000_000) - ) - - router, tickMap, takerFeeMap := s.setupMainnetRouter(config) - - routes := s.constructRoutesFromMainnetPools(router, USDT, UMEE, tickMap, takerFeeMap) - - quote, err := router.GetOptimalQuote(sdk.NewCoin(USDT, amountIn), routes) - - // We only validate that error does not occur without actually validating the quote. - s.Require().NoError(err) - - // TODO: update mainnet state and validate the quote - quoteRoutes := quote.GetRoute() - s.Require().Len(quoteRoutes, 1) -} - -func (s *RouterTestSuite) TestGetBestSplitRoutesQuote_Mainnet_UOSMOUION() { - config := defaultRouterConfig - config.MaxPoolsPerRoute = 5 - config.MaxRoutes = 10 - - var ( - amountIn = osmomath.NewInt(5000000) - ) - - router, tickMap, takerFeeMap := s.setupMainnetRouter(config) - - routes := s.constructRoutesFromMainnetPools(router, UOSMO, UION, tickMap, takerFeeMap) - - quote, err := router.GetOptimalQuote(sdk.NewCoin(UOSMO, amountIn), routes) - - // We only validate that error does not occur without actually validating the quote. - s.Require().NoError(err) - - // Expecting 1 route based on mainnet state - s.Require().Len(quote.GetRoute(), 1) - - // Should be pool 2 based on state at the time of writing. - // If new state is added, review the quote against mainnet. - s.Require().Len(quote.GetRoute()[0].GetPools(), 1) - - // Validate that the pool is pool 2 - s.Require().Equal(uint64(2), quote.GetRoute()[0].GetPools()[0].GetId()) -} - -func (s *RouterTestSuite) TestGetBestSplitRoutesQuote_Mainnet_USDTATOM() { - config := defaultRouterConfig - config.MaxPoolsPerRoute = 4 - config.MaxRoutes = 5 - config.MaxSplitRoutes = 3 - config.MaxSplitIterations = 10 - - var ( - amountIn = osmomath.NewInt(1000000) - ) - - router, tickMap, takerFeeMap := s.setupMainnetRouter(config) +// Validates that quotes constructed from mainnet state can be computed with no error +// for selected pairs. +func (s *RouterTestSuite) TestGetOptimalQuote() { + tests := map[string]struct { + tokenInDenom string + tokenOutDenom string - routes := s.constructRoutesFromMainnetPools(router, USDT, ATOM, tickMap, takerFeeMap) + maxPoolsPerRoute int + maxRoutes int + maxSplitRoutes int + maxSplitIterations int - quote, err := router.GetOptimalQuote(sdk.NewCoin(USDT, amountIn), routes) + amountIn osmomath.Int - // We only validate that error does not occur without actually validating the quote. - s.Require().NoError(err) + expectedRoutesCount int + }{ + // This pair originally caused an error due to the lack of filtering that was + // added later. + "usdt for umee": { + tokenInDenom: USDT, + tokenOutDenom: UMEE, - s.Require().NotNil(quote) + maxPoolsPerRoute: 5, + maxRoutes: 10, - s.Require().Greater(len(quote.GetRoute()), 0) -} + amountIn: osmomath.NewInt(1000_000_000), -func (s *RouterTestSuite) TestGetBestSplitRoutesQuote_Mainnet_AKTUMEE() { - config := defaultRouterConfig - config.MaxPoolsPerRoute = 4 - config.MaxRoutes = 10 - config.MaxSplitRoutes = 3 - - var ( - amountIn = osmomath.NewInt(100_000_000) - ) + expectedRoutesCount: 1, + }, + "uosmo for uion": { + tokenInDenom: UOSMO, + tokenOutDenom: UION, - router, tickMap, takerFeeMap := s.setupMainnetRouter(config) + maxPoolsPerRoute: 5, + maxRoutes: 10, - routes := s.constructRoutesFromMainnetPools(router, AKT, UMEE, tickMap, takerFeeMap) + amountIn: osmomath.NewInt(5000000), - quote, err := router.GetOptimalQuote(sdk.NewCoin(AKT, amountIn), routes) + expectedRoutesCount: 1, + }, + "usdt for atom": { + tokenInDenom: USDT, + tokenOutDenom: ATOM, - // We only validate that error does not occur without actually validating the quote. - s.Require().NoError(err) + maxPoolsPerRoute: 5, + maxRoutes: 10, + maxSplitRoutes: 3, - // Expecting 1 route based on mainnet state - s.Require().Len(quote.GetRoute(), 1) + amountIn: osmomath.NewInt(5000000), - route := quote.GetRoute()[0] - // Expecting 3 pools in the route - s.Require().Len(route.GetPools(), 3) + expectedRoutesCount: 1, + }, + "uakt for umee": { + tokenInDenom: AKT, + tokenOutDenom: UMEE, - // Validate that the pool is pool 1093 - s.Require().Equal(uint64(1093), route.GetPools()[0].GetId()) + maxPoolsPerRoute: 4, + maxRoutes: 10, + maxSplitRoutes: 3, - // Validate that the pool is pool 1077 - s.Require().Equal(uint64(1077), route.GetPools()[1].GetId()) + amountIn: osmomath.NewInt(100_000_000), - // Validate that the pool is pool 1205 - s.Require().Equal(uint64(1205), route.GetPools()[2].GetId()) -} + expectedRoutesCount: 2, + }, + // This test validates that with a greater max routes value, SQS is able to find + // the path from umee to stOsmo + "umee for stosmo": { + tokenInDenom: UMEE, + tokenOutDenom: stOSMO, -// This test validates that with a greater max routes value, SQS is able to find -// the path from umee to stOsmo -func (s *RouterTestSuite) TestGetBestSplitRoutesQuote_Mainnet_UMEEStOsmo() { - config := defaultRouterConfig - config.MaxPoolsPerRoute = 4 - // Note that max routes is set to 20 - config.MaxRoutes = 20 - config.MaxSplitRoutes = 3 + maxPoolsPerRoute: 4, + maxRoutes: 20, + maxSplitRoutes: 3, - var ( - amountIn = osmomath.NewInt(1_000_000) - ) + amountIn: osmomath.NewInt(1_000_000), - router, tickMap, takerFeeMap := s.setupMainnetRouter(config) + expectedRoutesCount: 1, + }, - routes := s.constructRoutesFromMainnetPools(router, UMEE, stOSMO, tickMap, takerFeeMap) + "atom for akt": { + tokenInDenom: ATOM, + tokenOutDenom: AKT, - quote, err := router.GetOptimalQuote(sdk.NewCoin(UMEE, amountIn), routes) + maxPoolsPerRoute: 4, + maxRoutes: 20, + maxSplitRoutes: 3, - // We only validate that error does not occur without actually validating the quote. - s.Require().NoError(err) + amountIn: osmomath.NewInt(1_000_000), - s.Require().NotNil(quote.GetAmountOut()) -} + expectedRoutesCount: 1, + }, + } -func (s *RouterTestSuite) TestGetBestSplitRoutesQuote_Mainnet_ATOMAKT() { - config := defaultRouterConfig - config.MaxPoolsPerRoute = 4 - // Note that max routes is set to 20 - config.MaxRoutes = 40 - config.MaxSplitRoutes = 3 - config.MinOSMOLiquidity = 10000 + for name, tc := range tests { + tc := tc + s.Run(name, func() { + // Setup router config + config := defaultRouterConfig + config.MaxPoolsPerRoute = tc.maxPoolsPerRoute + config.MaxRoutes = tc.maxRoutes + if tc.maxSplitRoutes > 0 { + config.MaxSplitRoutes = tc.maxSplitRoutes + } - var ( - amountIn = osmomath.NewInt(10_000_000) - ) + // Setup mainnet router + router, tickMap, takerFeeMap := s.setupMainnetRouter(config) - router, tickMap, takerFeeMap := s.setupMainnetRouter(config) + // Mock router use case. + routerUsecase, _ := s.setupRouterAndPoolsUsecase(router, tc.tokenInDenom, tc.tokenOutDenom, tickMap, takerFeeMap) - routes := s.constructRoutesFromMainnetPools(router, ATOM, AKT, tickMap, takerFeeMap) + // System under test + quote, err := routerUsecase.GetOptimalQuote(context.Background(), sdk.NewCoin(tc.tokenInDenom, tc.amountIn), tc.tokenOutDenom) - quote, _, err := router.EstimateBestSingleRouteQuote(routes, sdk.NewCoin(ATOM, amountIn)) + // We only validate that error does not occur without actually validating the quote. + s.Require().NoError(err) - // We only validate that error does not occur without actually validating the quote. - s.Require().NoError(err) + // TODO: update mainnet state and validate the quote for each test stricter. + quoteRoutes := quote.GetRoute() + s.Require().Len(quoteRoutes, tc.expectedRoutesCount) - s.Require().NotNil(quote.GetAmountOut()) + // Validate that the quote is not nil + s.Require().NotNil(quote.GetAmountOut()) + }) + } } // Validates custom quote for UOSMO to UION. @@ -772,6 +734,20 @@ func (s *RouterTestSuite) TestGetCustomQuote_Mainnet_UOSMOUION() { // - converting candidate routes to routes with all the necessary data. // COTRACT: router is initialized with setupMainnetRouter(...) or setupDefaultMainnetRouter(...) func (s *RouterTestSuite) constructRoutesFromMainnetPools(router *routerusecase.Router, tokenInDenom, tokenOutDenom string, tickMap map[uint64]domain.TickModel, takerFeeMap domain.TakerFeeMap) []route.RouteImpl { + _, poolsUsecase := s.setupRouterAndPoolsUsecase(router, tokenInDenom, tokenOutDenom, tickMap, takerFeeMap) + + candidateRoutes, err := router.GetCandidateRoutes(tokenInDenom, tokenOutDenom) + s.Require().NoError(err) + + routes, err := poolsUsecase.GetRoutesFromCandidates(context.Background(), candidateRoutes, takerFeeMap, tokenInDenom, tokenOutDenom) + s.Require().NoError(err) + + return routes +} + +// Sets up and returns usecases for router and pools by mocking the mainnet data +// from json files. +func (s *RouterTestSuite) setupRouterAndPoolsUsecase(router *routerusecase.Router, tokenInDenom, tokenOutDenom string, tickMap map[uint64]domain.TickModel, takerFeeMap domain.TakerFeeMap) (mvc.RouterUsecase, mvc.PoolsUsecase) { // Setup router repository mock routerRepositoryMock := mocks.RedisRouterRepositoryMock{} routerusecase.WithRouterRepository(router, &routerRepositoryMock) @@ -784,11 +760,7 @@ func (s *RouterTestSuite) constructRoutesFromMainnetPools(router *routerusecase. poolsUsecase := poolsusecase.NewPoolsUsecase(time.Hour, &poolsRepositoryMock, nil) routerusecase.WithPoolsUsecase(router, poolsUsecase) - candidateRoutes, err := router.GetCandidateRoutes(tokenInDenom, tokenOutDenom) - s.Require().NoError(err) - - routes, err := poolsUsecase.GetRoutesFromCandidates(context.Background(), candidateRoutes, takerFeeMap, tokenInDenom, tokenOutDenom) - s.Require().NoError(err) + routerUsecase := usecase.NewRouterUsecase(time.Hour, &routerRepositoryMock, poolsUsecase, defaultRouterConfig, &log.NoOpLogger{}) - return routes + return routerUsecase, poolsUsecase } diff --git a/ingest/sqs/router/usecase/router_usecase.go b/ingest/sqs/router/usecase/router_usecase.go index 4fcc404cb50..e6e9d0c20cd 100644 --- a/ingest/sqs/router/usecase/router_usecase.go +++ b/ingest/sqs/router/usecase/router_usecase.go @@ -2,6 +2,7 @@ package usecase import ( "context" + "errors" "fmt" "time" @@ -39,39 +40,158 @@ func NewRouterUsecase(timeout time.Duration, routerRepository mvc.RouterReposito // GetOptimalQuote returns the optimal quote by estimating the optimal route(s) through pools // on the osmosis network. +// Uses caching strategies for optimal performance. +// Currently, supports candidate route caching. If candidate routes for the given token in and token out denoms +// are present in cache, they are used without re-computing them. Otherwise, they are computed and cached. +// In the future, we will support caching of ranked routes that are constructed from candidate and sorted +// by the decreasing amount out within an order of magnitude of token in. Similarly, We will also support optimal split caching +// Returns error if: +// - fails to estimate direct quotes for ranked routes +// - fails to retrieve candidate routes +// - func (r *routerUseCaseImpl) GetOptimalQuote(ctx context.Context, tokenIn sdk.Coin, tokenOutDenom string) (domain.Quote, error) { + // TODO: implement and check ranked route cache + hasRankedRoutesInCache := false + + var ( + rankedRoutes []route.RouteImpl + topSingleRouteQuote domain.Quote + err error + ) + router := r.initializeRouter() - candidateRoutes, err := r.handleRoutes(ctx, router, tokenIn.Denom, tokenOutDenom) + if hasRankedRoutesInCache { + // TODO: if top routes are present in cache, estimate the quotes and return the best. + topSingleRouteQuote, rankedRoutes, err = estimateDirectQuote(router, rankedRoutes, tokenIn) + if err != nil { + return nil, err + } + } else { + // If top routes are not present in cache, retrieve unranked candidate routes + candidateRoutes, err := r.handleCandidateRoutes(ctx, router, tokenIn.Denom, tokenOutDenom) + if err != nil { + r.logger.Error("error handling routes", zap.Error(err)) + return nil, err + } + + for _, route := range candidateRoutes.Routes { + r.logger.Debug("filtered_candidate_route", zap.Any("route", route)) + } + + // Rank candidate routes by estimating direct quotes + topSingleRouteQuote, rankedRoutes, err = r.rankRoutesByDirectQuote(ctx, router, candidateRoutes, tokenIn, tokenOutDenom) + if err != nil { + r.logger.Error("error getting top routes", zap.Error(err)) + return nil, err + } + + if len(rankedRoutes) == 0 { + return nil, fmt.Errorf("no ranked routes found") + } + + // TODO: Cache ranked routes + } + + if len(rankedRoutes) == 1 { + return topSingleRouteQuote, nil + } + + // Compute split route quote + topSplitQuote, err := router.GetSplitQuote(rankedRoutes, tokenIn) if err != nil { - r.logger.Error("error handling routes", zap.Error(err)) return nil, err } - for _, route := range candidateRoutes.Routes { - r.logger.Debug("filtered_candidate_route", zap.Any("route", route)) + // TODO: Cache split route proportions + + finalQuote := topSingleRouteQuote + + // If the split route quote is better than the single route quote, return the split route quote + if topSplitQuote.GetAmountOut().GT(topSingleRouteQuote.GetAmountOut()) { + routes := topSplitQuote.GetRoute() + + r.logger.Debug("split route selected", zap.Int("route_count", len(routes))) + for _, route := range routes { + r.logger.Debug("route", zap.Stringer("route", route)) + } + + finalQuote = topSplitQuote + } + + r.logger.Debug("single route selected", zap.Stringer("route", finalQuote.GetRoute()[0])) + + if finalQuote.GetAmountOut().IsZero() { + return nil, errors.New("best we can do is no tokens out") } + return finalQuote, nil +} + +// rankRoutesByDirectQuote ranks the given candidate routes by estimating direct quotes over each route. +// Returns the top quote as well as the ranked routes in decrease order of amount out. +// Returns error if: +// - fails to read taker fees +// - fails to convert candidate routes to routes +// - fails to estimate direct quotes +func (r *routerUseCaseImpl) rankRoutesByDirectQuote(ctx context.Context, router *Router, candidateRoutes route.CandidateRoutes, tokenIn sdk.Coin, tokenOutDenom string) (domain.Quote, []route.RouteImpl, error) { // Note that retrieving pools and taker fees is done in separate transactions. // This is fine because taker fees don't change often. + // TODO: this can be refactored to only retrieve the relevant taker fees. takerFees, err := r.routerRepository.GetAllTakerFees(ctx) if err != nil { - return nil, err + return nil, nil, err } routes, err := r.poolsUsecase.GetRoutesFromCandidates(ctx, candidateRoutes, takerFees, tokenIn.Denom, tokenOutDenom) if err != nil { - return nil, err + return nil, nil, err + } + + topQuote, routes, err := estimateDirectQuote(router, routes, tokenIn) + if err != nil { + return nil, nil, err + } + + return topQuote, routes, nil +} + +// estimateDirectQuote estimates and returns the direct quote for the given routes, token in and token out denom. +// Also, returns the routes ranked by amount out in decreasing order. +// Returns error if: +// - fails to estimate direct quotes +func estimateDirectQuote(router *Router, routes []route.RouteImpl, tokenIn sdk.Coin) (domain.Quote, []route.RouteImpl, error) { + topQuote, routesSortedByAmtOut, err := router.estimateBestSingleRouteQuote(routes, tokenIn) + if err != nil { + return nil, nil, err + } + + numRoutes := len(routesSortedByAmtOut) + + // If split routes are disabled, return a single the top route + if router.maxSplitRoutes == 0 && numRoutes > 0 { + numRoutes = 1 + // If there are more routes than the max split routes, keep only the top routes + } else if len(routesSortedByAmtOut) > router.maxSplitRoutes { + // Keep only top routes for splits + routes = routes[:router.maxSplitRoutes] + numRoutes = router.maxSplitRoutes + } + + // Convert routes sorted by amount out to routes + for i := 0; i < numRoutes; i++ { + // Update routes with the top routes + routes[i] = routesSortedByAmtOut[i].RouteImpl } - return router.getOptimalQuote(tokenIn, routes) + return topQuote, routes, nil } // GetBestSingleRouteQuote returns the best single route quote to be done directly without a split. func (r *routerUseCaseImpl) GetBestSingleRouteQuote(ctx context.Context, tokenIn sdk.Coin, tokenOutDenom string) (domain.Quote, error) { router := r.initializeRouter() - candidateRoutes, err := r.handleRoutes(ctx, router, tokenIn.Denom, tokenOutDenom) + candidateRoutes, err := r.handleCandidateRoutes(ctx, router, tokenIn.Denom, tokenOutDenom) if err != nil { return nil, err } @@ -95,7 +215,7 @@ func (r *routerUseCaseImpl) GetCustomQuote(ctx context.Context, tokenIn sdk.Coin // TODO: abstract this router := r.initializeRouter() - candidateRoutes, err := r.handleRoutes(ctx, router, tokenIn.Denom, tokenOutDenom) + candidateRoutes, err := r.handleCandidateRoutes(ctx, router, tokenIn.Denom, tokenOutDenom) if err != nil { return nil, err } @@ -165,7 +285,7 @@ func (r *routerUseCaseImpl) GetCustomQuote(ctx context.Context, tokenIn sdk.Coin func (r *routerUseCaseImpl) GetCandidateRoutes(ctx context.Context, tokenInDenom string, tokenOutDenom string) (route.CandidateRoutes, error) { router := r.initializeRouter() - routes, err := r.handleRoutes(ctx, router, tokenInDenom, tokenOutDenom) + routes, err := r.handleCandidateRoutes(ctx, router, tokenInDenom, tokenOutDenom) if err != nil { return route.CandidateRoutes{}, err } @@ -234,14 +354,14 @@ func (r *routerUseCaseImpl) initializeRouter() *Router { return router } -// handleRoutes attempts to retrieve routes from the cache. If no routes are cached, it will +// handleCandidateRoutes attempts to retrieve candidate routes from the cache. If no routes are cached, it will // compute, persist in cache and return them. // Returns routes on success // Errors if: // - there is an error retrieving routes from cache // - there are no routes cached and there is an error computing them // - fails to persist the computed routes in cache -func (r *routerUseCaseImpl) handleRoutes(ctx context.Context, router *Router, tokenInDenom, tokenOutDenom string) (candidateRoutes route.CandidateRoutes, err error) { +func (r *routerUseCaseImpl) handleCandidateRoutes(ctx context.Context, router *Router, tokenInDenom, tokenOutDenom string) (candidateRoutes route.CandidateRoutes, err error) { r.logger.Debug("getting routes") // Check cache for routes if enabled