Skip to content

BE-691 | Implement handleCandidateRoutesInGivenOut API #617

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

Open
wants to merge 1 commit into
base: v28.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
8 changes: 6 additions & 2 deletions router/usecase/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,12 @@ func ValidateAndFilterRoutesOutGivenIn(candidateRoutes []candidateRouteWrapper,
return validateAndFilterRoutesOutGivenIn(candidateRoutes, tokenInDenom, logger)
}

func (r *routerUseCaseImpl) HandleRoutes(ctx context.Context, tokenIn sdk.Coin, tokenOutDenom string, candidateRouteSearchOptions domain.CandidateRouteSearchOptions) (candidateRoutes ingesttypes.CandidateRoutes, err error) {
return r.handleCandidateRoutes(ctx, tokenIn, tokenOutDenom, candidateRouteSearchOptions)
func (r *routerUseCaseImpl) HandleRoutesOutGivenIn(ctx context.Context, tokenIn sdk.Coin, tokenOutDenom string, candidateRouteSearchOptions domain.CandidateRouteSearchOptions) (candidateRoutes ingesttypes.CandidateRoutes, err error) {
return r.handleCandidateRoutesOutGivenIn(ctx, tokenIn, tokenOutDenom, candidateRouteSearchOptions)
}

func (r *routerUseCaseImpl) HandleRoutesInGivenOut(ctx context.Context, tokenOut sdk.Coin, tokenInDenom string, candidateRouteSearchOptions domain.CandidateRouteSearchOptions) (candidateRoutes ingesttypes.CandidateRoutes, err error) {
return r.handleCandidateRoutesInGivenOut(ctx, tokenOut, tokenInDenom, candidateRouteSearchOptions)
}

func (r *routerUseCaseImpl) EstimateAndRankSingleRouteQuoteOutGivenIn(ctx context.Context, routes []route.RouteImpl, tokenIn sdk.Coin, logger log.Logger) (domain.Quote, []RouteWithOutAmount, error) {
Expand Down
58 changes: 54 additions & 4 deletions router/usecase/router_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ func (r *routerUseCaseImpl) computeAndRankRoutesByDirectQuote(ctx context.Contex
}

// If top routes are not present in cache, retrieve unranked candidate routes
candidateRoutes, err := r.handleCandidateRoutes(ctx, tokenIn, tokenOutDenom, candidateRouteSearchOptions)
candidateRoutes, err := r.handleCandidateRoutesOutGivenIn(ctx, tokenIn, tokenOutDenom, candidateRouteSearchOptions)
if err != nil {
r.logger.Error("error handling routes", zap.Error(err))
return nil, nil, err
Expand Down Expand Up @@ -561,7 +561,7 @@ func (r *routerUseCaseImpl) GetCandidateRoutes(ctx context.Context, tokenIn sdk.
candidateRouteSearchOptions.MinPoolLiquidityCap = r.ConvertMinTokensPoolLiquidityCapToFilter(dynamicMinPoolLiquidityCap)
}

candidateRoutes, err := r.handleCandidateRoutes(ctx, tokenIn, tokenOutDenom, candidateRouteSearchOptions)
candidateRoutes, err := r.handleCandidateRoutesOutGivenIn(ctx, tokenIn, tokenOutDenom, candidateRouteSearchOptions)
if err != nil {
return ingesttypes.CandidateRoutes{}, err
}
Expand Down Expand Up @@ -664,14 +664,14 @@ func (r *routerUseCaseImpl) GetCachedRankedRoutes(ctx context.Context, method do
return rankedRoutes, nil
}

// handleCandidateRoutes attempts to retrieve candidate routes from the cache. If no routes are cached, it will
// handleCandidateRoutesOutGivenIn 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) handleCandidateRoutes(ctx context.Context, tokenIn sdk.Coin, tokenOutDenom string, candidateRouteSearchOptions domain.CandidateRouteSearchOptions) (candidateRoutes ingesttypes.CandidateRoutes, err error) {
func (r *routerUseCaseImpl) handleCandidateRoutesOutGivenIn(ctx context.Context, tokenIn sdk.Coin, tokenOutDenom string, candidateRouteSearchOptions domain.CandidateRouteSearchOptions) (candidateRoutes ingesttypes.CandidateRoutes, err error) {
r.logger.Debug("getting routes")

// Check cache for routes if enabled
Expand Down Expand Up @@ -714,6 +714,56 @@ func (r *routerUseCaseImpl) handleCandidateRoutes(ctx context.Context, tokenIn s
return candidateRoutes, nil
}

// handleCandidateRoutesInGivenOut 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) handleCandidateRoutesInGivenOut(ctx context.Context, tokenOut sdk.Coin, tokenInDenom string, candidateRouteSearchOptions domain.CandidateRouteSearchOptions) (candidateRoutes ingesttypes.CandidateRoutes, err error) { // nolint:unused
r.logger.Debug("getting routes")

// Check cache for routes if enabled
var isFoundCached bool
if !candidateRouteSearchOptions.DisableCache {
candidateRoutes, isFoundCached, err = r.GetCachedCandidateRoutes(ctx, domain.TokenSwapMethodExactOut, tokenOut.Denom, tokenInDenom)
if err != nil {
return ingesttypes.CandidateRoutes{}, err
}
}

r.logger.Debug("cached routes", zap.Int("num_routes", len(candidateRoutes.Routes)))

// If no routes are cached, find them
if !isFoundCached {
r.logger.Debug("calculating routes")

candidateRoutes, err = r.candidateRouteSearcher.FindCandidateRoutesInGivenOut(tokenOut, tokenInDenom, candidateRouteSearchOptions)
if err != nil {
r.logger.Error("error getting candidate routes for pricing", zap.Error(err))
return ingesttypes.CandidateRoutes{}, err
}

r.logger.Info("calculated routes", zap.Int("num_routes", len(candidateRoutes.Routes)))

// Persist routes
if !candidateRouteSearchOptions.DisableCache {
cacheDurationSeconds := r.defaultConfig.CandidateRouteCacheExpirySeconds
if len(candidateRoutes.Routes) == 0 {
// If there are no routes, we want to cache the result for a shorter duration
// Add 1 to ensure that it is never 0 as zero signifies never clearing.
cacheDurationSeconds = cacheDurationSeconds/4 + 1
}

r.logger.Debug("persisting routes", zap.Int("num_routes", len(candidateRoutes.Routes)))
r.candidateRouteCache.Set(formatCandidateRouteCacheKey(domain.TokenSwapMethodExactOut, tokenOut.Denom, tokenInDenom), candidateRoutes, time.Duration(cacheDurationSeconds)*time.Second)
}
}

return candidateRoutes, nil
}

// StoreRouterStateFiles implements domain.RouterUsecase.
// TODO: clean up
func (r *routerUseCaseImpl) StoreRouterStateFiles() error {
Expand Down
267 changes: 265 additions & 2 deletions router/usecase/router_usecase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ var (

// Tests the call to handleRoutes by mocking the router repository and pools use case
// with relevant data.
func (s *RouterTestSuite) TestHandleRoutes() {
func (s *RouterTestSuite) TestHandleRoutesOutGivenIn() {
const (
defaultTimeoutDuration = time.Second * 10

Expand Down Expand Up @@ -310,7 +310,7 @@ func (s *RouterTestSuite) TestHandleRoutes() {
}

// System under test
actualCandidateRoutes, err := routerUseCaseImpl.HandleRoutes(ctx, sdk.NewCoin(tokenInDenom, one), tokenOutDenom, candidateRouteSearchOptions)
actualCandidateRoutes, err := routerUseCaseImpl.HandleRoutesOutGivenIn(ctx, sdk.NewCoin(tokenInDenom, one), tokenOutDenom, candidateRouteSearchOptions)

if tc.expectedError != nil {
s.Require().EqualError(err, tc.expectedError.Error())
Expand Down Expand Up @@ -350,6 +350,269 @@ func (s *RouterTestSuite) TestHandleRoutes() {
}
}

func (s *RouterTestSuite) TestHandleRoutesInGivenOut() {
const (
defaultTimeoutDuration = time.Second * 10

tokenOutDenom = "uosmo"
tokenInDenom = "uion"

minPoolLiquidityCap = 100
)

// Create test balancer pool

balancerCoins := sdk.NewCoins(
sdk.NewCoin(tokenOutDenom, osmomath.NewInt(1000000000000000000)),
sdk.NewCoin(tokenInDenom, osmomath.NewInt(1000000000000000000)),
)

balancerPoolID := s.PrepareBalancerPoolWithCoins(balancerCoins...)
balancerPool, err := s.App.PoolManagerKeeper.GetPool(s.Ctx, balancerPoolID)
s.Require().NoError(err)

defaultPool := &ingesttypes.PoolWrapper{
ChainModel: balancerPool,
SQSModel: ingesttypes.SQSPool{
PoolLiquidityCap: osmomath.NewInt(int64(minPoolLiquidityCap*OsmoPrecisionMultiplier + 1)),
PoolDenoms: []string{tokenOutDenom, tokenInDenom},
Balances: balancerCoins,
SpreadFactor: DefaultSpreadFactor,
},
}

var (
defaultRoute = WithCandidateRoutePools(
EmptyCandidateRoute,
[]ingesttypes.CandidatePool{
{
ID: defaultPool.GetId(),
TokenOutDenom: tokenInDenom,
},
},
)

recomputedRoute = WithCandidateRoutePools(
EmptyCandidateRoute,
[]ingesttypes.CandidatePool{
{
ID: defaultPool.GetId() + 1,
TokenOutDenom: tokenInDenom,
},
},
)

singleDefaultRoutes = ingesttypes.CandidateRoutes{
Routes: []ingesttypes.CandidateRoute{defaultRoute},
UniquePoolIDs: map[uint64]struct{}{
defaultPool.GetId(): {},
},
}

singeldRecomputedRoutes = ingesttypes.CandidateRoutes{
Routes: []ingesttypes.CandidateRoute{recomputedRoute},
UniquePoolIDs: map[uint64]struct{}{
defaultPool.GetId() + 1: {},
},
}

emptyRoutes = ingesttypes.CandidateRoutes{}

defaultRouterConfig = domain.RouterConfig{
// Only these config values are relevant for this test
// for searching for routes when none were present in cache.
MaxPoolsPerRoute: 4,
MaxRoutes: 4,

// These configs are not relevant for this test.
PreferredPoolIDs: []uint64{},
MinPoolLiquidityCap: minPoolLiquidityCap,
}
)

testCases := []struct {
name string

repositoryRoutes ingesttypes.CandidateRoutes
takerFeeMap ingesttypes.TakerFeeMap
// specifies if the config applying to all requests disables
// the cache.
isCacheConfigDisabled bool
// specifies if request-specific option disables the cache.
isCacheOptionDisabled bool
shouldSkipAddToCache bool

expectedCandidateRoutes ingesttypes.CandidateRoutes

expectedError error
expectedIsCached bool
}{
{
name: "routes in cache -> use them",

repositoryRoutes: singleDefaultRoutes,

expectedCandidateRoutes: singleDefaultRoutes,
expectedIsCached: true,
},
{
name: "routes in cache but cache is disabled via options -> use them",

repositoryRoutes: singleDefaultRoutes,

isCacheOptionDisabled: true,

expectedCandidateRoutes: emptyRoutes,
},
{
name: "cache is disabled in config -> recomputes routes despite having available in cache",

repositoryRoutes: singleDefaultRoutes,
isCacheConfigDisabled: true,

expectedCandidateRoutes: singeldRecomputedRoutes,
expectedIsCached: false,
},
{
name: "cache is disabled in config but option turns it back on -> get routes",

repositoryRoutes: singleDefaultRoutes,

isCacheOptionDisabled: false,

expectedCandidateRoutes: singleDefaultRoutes,
expectedIsCached: true,
},
{
name: "no routes in cache -> recomputes routes & caches them",

repositoryRoutes: emptyRoutes,
shouldSkipAddToCache: true,

expectedCandidateRoutes: singeldRecomputedRoutes,
expectedIsCached: true,
},
{
name: "no routes in cache -> recomputes routes & but does not cache them due to option disablement",

repositoryRoutes: emptyRoutes,
shouldSkipAddToCache: true,

isCacheOptionDisabled: true,

expectedCandidateRoutes: singeldRecomputedRoutes,
expectedIsCached: false,
},
{
name: "empty routes in cache-> does not recompute routes",

repositoryRoutes: emptyRoutes,

expectedCandidateRoutes: emptyRoutes,
expectedIsCached: true,
},
{
name: "no routes in cache and fails to recompute -> returns no routes & caches them",

repositoryRoutes: emptyRoutes,

expectedCandidateRoutes: emptyRoutes,
expectedIsCached: true,
},
{
name: "no routes in cache and fails to recompute but option disables cache -> returns no routes and does not cache",

repositoryRoutes: emptyRoutes,
isCacheOptionDisabled: true,

expectedCandidateRoutes: emptyRoutes,
expectedIsCached: false,
},

// TODO:
// routes in cache but pools have more optimal -> cache is still used
// multiple routes in cache -> use them
// multiple rotues in pools -> use them
// error in repository -> return error
// error in storing routes after recomputing -> return error
}

for _, tc := range testCases {
tc := tc
s.Run(tc.name, func() {

routerRepositoryMock := routerrepo.New(&log.NoOpLogger{})

candidateRouteCache := cache.New()

if !tc.shouldSkipAddToCache {
candidateRouteCache.Set(usecase.FormatCandidateRouteCacheKey(domain.TokenSwapMethodExactOut, tokenOutDenom, tokenInDenom), tc.repositoryRoutes, time.Hour)
}

poolsUseCaseMock := &mocks.PoolsUsecaseMock{}

tokenMetaDataHolder := mocks.TokenMetadataHolderMock{}
candidateRouteFinderMock := mocks.CandidateRouteFinderMock{
Routes: tc.expectedCandidateRoutes,
}

routerUseCase := usecase.NewRouterUsecase(routerRepositoryMock, poolsUseCaseMock, candidateRouteFinderMock, &tokenMetaDataHolder, domain.RouterConfig{
RouteCacheEnabled: !tc.isCacheConfigDisabled,
}, emptyCosmWasmPoolsRouterConfig, &log.NoOpLogger{}, cache.New(), candidateRouteCache)

routerUseCaseImpl, ok := routerUseCase.(*usecase.RouterUseCaseImpl)
s.Require().True(ok)

ctx := context.Background()

candidateRouteSearchOptions := domain.CandidateRouteSearchOptions{
MinPoolLiquidityCap: minPoolLiquidityCap,
MaxRoutes: defaultRouterConfig.MaxRoutes,
MaxPoolsPerRoute: defaultRouterConfig.MaxPoolsPerRoute,
DisableCache: tc.isCacheOptionDisabled,
}

// System under test
actualCandidateRoutes, err := routerUseCaseImpl.HandleRoutesInGivenOut(ctx, sdk.NewCoin(tokenOutDenom, one), tokenInDenom, candidateRouteSearchOptions)

if tc.expectedError != nil {
s.Require().EqualError(err, tc.expectedError.Error())
s.Require().Len(actualCandidateRoutes, 0)
return
}

s.Require().NoError(err)

// Pre-set routes should be returned.

s.Require().Equal(len(tc.expectedCandidateRoutes.Routes), len(actualCandidateRoutes.Routes))
for i, route := range actualCandidateRoutes.Routes {
s.Require().Equal(tc.expectedCandidateRoutes.Routes[i], route)
}

// If cache option is being tested, getting the cached candidate routes is ineligible for the test by-design.
if tc.isCacheOptionDisabled {
return
}

cachedCandidateRoutes, isCached, err := routerUseCaseImpl.GetCachedCandidateRoutes(ctx, domain.TokenSwapMethodExactOut, tokenOutDenom, tokenInDenom)

if tc.isCacheConfigDisabled {
s.Require().NoError(err)
s.Require().Empty(cachedCandidateRoutes.Routes)
s.Require().False(isCached)
return
}

// For the case where the cache is disabled, the expected routes in cache
// will be the same as the original routes in the repository.
// Check that router repository was updated
s.Require().Equal(tc.expectedCandidateRoutes, cachedCandidateRoutes)
s.Require().Equal(tc.expectedIsCached, isCached)
})
}
}

// Tests that routes that overlap in pools IDs get filtered out.
// Tests that the order of the routes is in decreasing priority.
// That is, if routes A and B overlap where A comes before B, then B is filtered out.
Expand Down
Loading