Skip to content

Commit

Permalink
feat: overwrite route recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
p0mvn committed Jan 9, 2024
1 parent f466fe1 commit 6af2d45
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 18 deletions.
6 changes: 6 additions & 0 deletions app/sidecar_query_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ func NewSideCarQueryServer(appCodec codec.Codec, routerConfig domain.RouterConfi
routerUsecase := routerUseCase.WithOverwriteRoutesPath(routerUseCase.NewRouterUsecase(timeoutContext, routerRepository, poolsUseCase, routerConfig, logger, cache.New(), routesOverwrite), overwriteRoutesPath)
routerHttpDelivery.NewRouterHandler(e, routerUsecase, logger)

// Load overwrite routes from disk if they exist.
err = routerUsecase.LoadOverwriteRoutes(ctx)
if err != nil {
return nil, err
}

// Initialize system handler
chainInfoRepository := chaininforedisrepo.New(redisTxManager)
chainInfoUseCase := chaininfousecase.NewChainInfoUsecase(timeoutContext, chainInfoRepository, redisTxManager)
Expand Down
5 changes: 5 additions & 0 deletions domain/mvc/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,9 @@ type RouterUsecase interface {
// * Denom does not exist in pool
// * Token out mismatch across routes
OverwriteRoutes(ctx context.Context, tokeinInDenom string, candidateRoutes []sqsdomain.CandidateRoute) error
// LoadOverwriteRoutes loads the overwrite routes from disk if they exist.
// If they do not exist, this is a no-op.
// If they exist, it loads them into the router usecase.
// Returns errors if any.
LoadOverwriteRoutes(ctx context.Context) error
}
62 changes: 60 additions & 2 deletions router/usecase/router_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"errors"
"fmt"
"net/url"
"os"
"strings"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand Down Expand Up @@ -48,6 +50,8 @@ type routerUseCaseImpl struct {
const (
candidateRouteCacheLabel = "candidate_route"
rankedRouteCacheLabel = "ranked_route"

denomSeparatorChar = "|"
)

var (
Expand Down Expand Up @@ -664,14 +668,68 @@ func (r *routerUseCaseImpl) OverwriteRoutes(ctx context.Context, tokeinInDenom s
return nil
}

// LoadOverwriteRoutes loads the overwrite routes from disk if they exist.
// If they do not exist, this is a no-op.
// If they exist, it loads them into the router usecase.
// Returns errors if any.
func (r *routerUseCaseImpl) LoadOverwriteRoutes(ctx context.Context) error {
// Read overwrite routes from disk if they exist.
_, err := os.Stat(r.overwriteRoutesPath)
if err != nil && err != os.ErrNotExist {
return err
} else if err == nil {
entries, err := os.ReadDir(r.overwriteRoutesPath)
if err != nil {
return err
}

for _, entry := range entries {
if entry.IsDir() {
return fmt.Errorf("overwrite routes directory should not contain subdirectories")
}

fileName := entry.Name()

// Read the entire file
content, err := os.ReadFile(fmt.Sprintf("%s/%s", r.overwriteRoutesPath, fileName))
if err != nil {
return err
}

// Parse to candidate routes
var candidateRoutes sqsdomain.CandidateRoutes
if err := json.Unmarshal(content, &candidateRoutes); err != nil {
return err
}

tokenInDenomTokenOutDenomStr, err := url.PathUnescape(fileName)
if err != nil {
return err
}

tokenInDenomTokenOutDenom := strings.Split(tokenInDenomTokenOutDenomStr, denomSeparatorChar)
if len(tokenInDenomTokenOutDenom) != 2 {
return fmt.Errorf("overwrite routes file name should be of format: '<tokenInDenom>%s<tokenOutDenom>.json URL-escaped", denomSeparatorChar)
}

tokenInDenom := tokenInDenomTokenOutDenom[0]

if err := r.OverwriteRoutes(ctx, tokenInDenom, candidateRoutes.Routes); err != nil {
return err
}
}
}
return nil
}

// formatRouteCacheKey formats the given token in and token out denoms to a string.
func formatRouteCacheKey(tokenInDenom string, tokenOutDenom string) string {
return fmt.Sprintf("%s/%s", tokenInDenom, tokenOutDenom)
return fmt.Sprintf("%s%s%s", tokenInDenom, denomSeparatorChar, tokenOutDenom)
}

// formatRankedRouteCacheKey formats the given token in and token out denoms and order of magnitude to a string.
func formatRankedRouteCacheKey(tokenInDenom string, tokenOutDenom string, tokenIOrderOfMagnitude int) string {
return fmt.Sprintf("%s/%d", formatRouteCacheKey(tokenInDenom, tokenOutDenom), tokenIOrderOfMagnitude)
return fmt.Sprintf("%s%s%d", formatRouteCacheKey(tokenInDenom, tokenOutDenom), denomSeparatorChar, tokenIOrderOfMagnitude)
}

// convertRankedToCandidateRoutes converts the given ranked routes to candidate routes.
Expand Down
53 changes: 37 additions & 16 deletions router/usecase/router_usecase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/osmosis-labs/sqs/domain"
"github.com/osmosis-labs/sqs/domain/cache"
"github.com/osmosis-labs/sqs/domain/mocks"
"github.com/osmosis-labs/sqs/domain/mvc"
"github.com/osmosis-labs/sqs/log"
"github.com/osmosis-labs/sqs/router/usecase"
"github.com/osmosis-labs/sqs/router/usecase/route"
Expand Down Expand Up @@ -619,11 +620,12 @@ func (s *RouterTestSuite) TestGetOptimalQuote_Cache_Overwrites() {

// Validate that the quote is not nil
s.Require().NotNil(quote.GetAmountOut())

})
}
}

// Basic happy path test for OverwriteRoutes.
// Basic happy path test for OverwriteRoutes and LoadOverwriteRoutes.
//
// Similar to TestGetOptimalQuote_Cache_Overwrites, this test is set up by focusing on ATOM / OSMO mainnet state pool.
// We restrict the number of routes via config.
Expand All @@ -632,7 +634,7 @@ func (s *RouterTestSuite) TestGetOptimalQuote_Cache_Overwrites() {
// Pool ID 1: https://app.osmosis.zone/pool/1 (balancer) 0.2% spread factor and 20M of liquidity to date
// Pool ID 1135: https://app.osmosis.zone/pool/1135 (concentrated) 0.2% spread factor and 14M of liquidity to date
// Pool ID 1265: https://app.osmosis.zone/pool/1265 (concentrated) 0.05% spread factor and 224K of liquidity to date
func (s *RouterTestSuite) TestOverwriteRoutes() {
func (s *RouterTestSuite) TestOverwriteRoutes_LoadOverwriteRoutes() {
const tempPath = "temp"

s.Setup()
Expand All @@ -647,40 +649,59 @@ func (s *RouterTestSuite) TestOverwriteRoutes() {

// Mock router use case.
routerUsecase, _ := s.setupRouterAndPoolsUsecase(router, tickMap, takerFeeMap, cache.New(), cache.NewRoutesOverwrite())

routerUsecase = usecase.WithOverwriteRoutesPath(routerUsecase, tempPath)

// Get quote without overwrite
quote, err := routerUsecase.GetOptimalQuote(context.Background(), sdk.NewCoin(UOSMO, defaultAmountInCache), ATOM)
s.Require().NoError(err)

// Without overwrite this is the pool ID we expect given the amount in.
s.Require().Equal(poolID1265Concentrated, quote.GetRoute()[0].GetPools()[0].GetId())
s.validatePoolIDInRoute(routerUsecase, sdk.NewCoin(UOSMO, defaultAmountInCache), ATOM, poolID1265Concentrated)

defer func() {
// Clean up
os.RemoveAll(tempPath)
}()

// System under test #1
err = routerUsecase.OverwriteRoutes(context.Background(), UOSMO, poolIDOneRoute.Routes)
s.Require().NoError(err)

// Get quote with overwrite
quote, err = routerUsecase.GetOptimalQuote(context.Background(), sdk.NewCoin(UOSMO, defaultAmountInCache), ATOM)
err := routerUsecase.OverwriteRoutes(context.Background(), UOSMO, poolIDOneRoute.Routes)
s.Require().NoError(err)

// With overwrite this is the pool ID we expect given the amount in.
s.Require().Equal(poolIDOneBalancer, quote.GetRoute()[0].GetPools()[0].GetId())
s.validatePoolIDInRoute(routerUsecase, sdk.NewCoin(UOSMO, defaultAmountInCache), ATOM, poolIDOneBalancer)

// Validate that the overwrite can be modified
// System under test #2
err = routerUsecase.OverwriteRoutes(context.Background(), UOSMO, poolID1135Route.Routes)
s.Require().NoError(err)

quote, err = routerUsecase.GetOptimalQuote(context.Background(), sdk.NewCoin(UOSMO, defaultAmountInCache), ATOM)
// With overwrite this is the pool ID we expect given the amount in.
s.validatePoolIDInRoute(routerUsecase, sdk.NewCoin(UOSMO, defaultAmountInCache), ATOM, poolID1135Concentrated)

// Now, drop the original use case and create a new one
routerUsecase, _ = s.setupRouterAndPoolsUsecase(router, tickMap, takerFeeMap, cache.New(), cache.NewRoutesOverwrite())
routerUsecase = usecase.WithOverwriteRoutesPath(routerUsecase, tempPath)

// // Without overwrite this is the pool ID we expect given the amount in.
s.validatePoolIDInRoute(routerUsecase, sdk.NewCoin(UOSMO, defaultAmountInCache), ATOM, poolID1265Concentrated)

// Load overwrite
err = routerUsecase.LoadOverwriteRoutes(context.Background())
s.Require().NoError(err)

// With overwrite this is the pool ID we expect given the amount in.
s.Require().Equal(poolID1135Concentrated, quote.GetRoute()[0].GetPools()[0].GetId())
s.validatePoolIDInRoute(routerUsecase, sdk.NewCoin(UOSMO, defaultAmountInCache), ATOM, poolID1135Concentrated)
}

// validates that for the given coinIn and tokenOutDenom, there is one route with one pool ID equal to the expectedPoolID.
// This helper is useful in specific tests that rely on this configuration.
func (s *RouterTestSuite) validatePoolIDInRoute(routerUseCase mvc.RouterUsecase, coinIn sdk.Coin, tokenOutDenom string, expectedPoolID uint64) {
// Get quote
quote, err := routerUseCase.GetOptimalQuote(context.Background(), coinIn, tokenOutDenom)
s.Require().NoError(err)

quoteRoutes := quote.GetRoute()
s.Require().Len(quoteRoutes, 1)

routePools := quoteRoutes[0].GetPools()
s.Require().Len(routePools, 1)

// Validate that the pool ID is the expected one
s.Require().Equal(expectedPoolID, routePools[0].GetId())
}

0 comments on commit 6af2d45

Please sign in to comment.