Skip to content
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

Allow unlisted denoms in the /custom-direct-quote #540

Draft
wants to merge 1 commit into
base: v26.x
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ Ref: https://keepachangelog.com/en/1.0.0/

# Changelog

## Unreleased

- Allow unlisted denoms in the /custom-direct-quote endpoint.

## v26.1.0

e42b32bc SQS-412 | Active Orders Query: SSE (#518)
Expand Down
9 changes: 9 additions & 0 deletions domain/mocks/tokens_usecase_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type TokensUsecaseMock struct {
UpdateAssetsAtHeightIntervalSyncFunc func(height uint64) error
SetTokenRegistryLoaderFunc func(loader domain.TokenRegistryLoader)
ClearPoolDenomMetadataFunc func()
IsValidListedDenomFunc func(chainDenom string) bool
}

var _ mvc.TokensUsecase = &TokensUsecaseMock{}
Expand Down Expand Up @@ -172,3 +173,11 @@ func (m *TokensUsecaseMock) ClearPoolDenomMetadata() {
}
panic("unimplemented")
}

// IsValidListedDenom implements mvc.TokensUsecase.
func (m *TokensUsecaseMock) IsValidListedDenom(chainDenom string) bool {
if m.IsValidListedDenomFunc != nil {
return m.IsValidListedDenomFunc(chainDenom)
}
return false
}
Comment on lines +177 to +183
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential panic in mock implementation.

The implementation returns false as default behavior, which is good. However, there's an inconsistency with other mock methods in this file that use panic("unimplemented") as their default behavior (see SetTokenRegistryLoader and ClearPoolDenomMetadata).

Consider either:

  1. Adding the panic for consistency:
 func (m *TokensUsecaseMock) IsValidListedDenom(chainDenom string) bool {
 	if m.IsValidListedDenomFunc != nil {
 		return m.IsValidListedDenomFunc(chainDenom)
 	}
-	return false
+	panic("unimplemented")
 }
  1. Or update other methods to return zero values instead of panicking for consistency across the mock.

Committable suggestion was skipped due to low confidence.

22 changes: 17 additions & 5 deletions domain/mvc/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,12 @@ type TokensUsecase interface {
// RegisterPricingStrategy registers a pricing strategy for a given pricing source.
RegisterPricingStrategy(source domain.PricingSourceType, strategy domain.PricingSource)

// IsValidChainDenom checks if the chain denom is valid
IsValidChainDenom(chainDenom string) bool

// IsValidListedDenom checks if the chain denom is valid
IsValidListedDenom(chainDenom string) bool

// IsValidPricingSource checks if the pricing source is a valid one
IsValidPricingSource(pricingSource int) bool

Expand All @@ -101,8 +105,10 @@ type TokensUsecase interface {
// ValidateChainDenomQueryParam validates the chain denom query parameter.
// If isHumanDenoms is true, it converts the human denom to chain denom.
// If isHumanDenoms is false, it validates the chain denom.
// If doesAllowUnlistedDenoms is true, it allows unlisted denoms.
// If doesAllowUnlistedDenoms is false, it does not allow unlisted denoms.
// Returns the chain denom and an error if any.
func ValidateChainDenomQueryParam(tokensUsecase TokensUsecase, denom string, isHumanDenoms bool) (string, error) {
func ValidateChainDenomQueryParam(tokensUsecase TokensUsecase, denom string, isHumanDenoms bool, doesAllowUnlistedDenoms bool) (string, error) {
// Note that sdk.Coins initialization
// auto-converts base denom from human
// to IBC notation.
Expand All @@ -120,8 +126,14 @@ func ValidateChainDenomQueryParam(tokensUsecase TokensUsecase, denom string, isH
return tokensUsecase.GetChainDenom(denom)
}
} else {
if !tokensUsecase.IsValidChainDenom(denom) {
return "", fmt.Errorf("denom is not a valid chain denom (%s)", denom)
if !doesAllowUnlistedDenoms {
if !tokensUsecase.IsValidListedDenom(denom) {
return "", fmt.Errorf("denom is not a valid listed chain denom (%s)", denom)
}
} else {
if !tokensUsecase.IsValidChainDenom(denom) {
return "", fmt.Errorf("denom is not a valid chain denom (%s)", denom)
}
}
}

Expand All @@ -130,15 +142,15 @@ func ValidateChainDenomQueryParam(tokensUsecase TokensUsecase, denom string, isH
}

// ValidateChainDenomsQueryParam validates the chain denom query parameters.
func ValidateChainDenomsQueryParam(c echo.Context, tokensUsecase TokensUsecase, denoms []string) ([]string, error) {
func ValidateChainDenomsQueryParam(c echo.Context, tokensUsecase TokensUsecase, denoms []string, doesAllowUnlistedDenoms bool) ([]string, error) {
isHumanDenoms, err := domain.GetIsHumanDenomsQueryParam(c)
if err != nil {
return nil, err
}

chainDenoms := make([]string, len(denoms))
for i, denom := range denoms {
chainDenom, err := ValidateChainDenomQueryParam(tokensUsecase, denom, isHumanDenoms)
chainDenom, err := ValidateChainDenomQueryParam(tokensUsecase, denom, isHumanDenoms, doesAllowUnlistedDenoms)
if err != nil {
return nil, err
}
Expand Down
162 changes: 162 additions & 0 deletions domain/mvc/tokens_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package mvc_test

import (
"errors"
"fmt"
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/osmosis-labs/sqs/domain/mocks"
"github.com/osmosis-labs/sqs/domain/mvc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
denom = "uatom"
ibcDenom = "ibc/" + denom
)

func TestValidateChainDenomQueryParam(t *testing.T) {

baseDenom, err := sdk.GetBaseDenom()
require.NoError(t, err)

defaultError := errors.New("default error")

// Test cases
tests := []struct {
name string
denom string
isHumanDenoms bool
doesAllowUnlistedDenoms bool
mock *mocks.TokensUsecaseMock
expectedDenom string
expectedError string
baseError bool // true if we expect base denom error
}{
{
name: "Error validating base denom",
denom: "not" + baseDenom,
isHumanDenoms: true,
doesAllowUnlistedDenoms: true,
mock: &mocks.TokensUsecaseMock{
GetChainDenomFunc: func(humanDenom string) (string, error) {
return "", defaultError
},
},
expectedDenom: "",
expectedError: defaultError.Error(),
},
{
name: "Human denom conversion success",
denom: denom,
isHumanDenoms: true,
doesAllowUnlistedDenoms: true,
mock: &mocks.TokensUsecaseMock{
GetChainDenomFunc: func(humanDenom string) (string, error) {
return "ibc/" + humanDenom, nil
},
},
expectedDenom: ibcDenom,
expectedError: "",
},
{
name: "Human denom conversion success",
denom: denom,
isHumanDenoms: true,
doesAllowUnlistedDenoms: true,
mock: &mocks.TokensUsecaseMock{
GetChainDenomFunc: func(humanDenom string) (string, error) {
return ibcDenom, nil
},
},
expectedDenom: ibcDenom,
expectedError: "",
},
{
name: "Human denom conversion error",
denom: "invalid",
isHumanDenoms: true,
doesAllowUnlistedDenoms: true,
mock: &mocks.TokensUsecaseMock{
GetChainDenomFunc: func(humanDenom string) (string, error) {
return "", fmt.Errorf("invalid denom")
},
},
expectedDenom: "",
expectedError: "invalid denom",
},
{
name: "Valid unlisted chain denom",
denom: ibcDenom,
isHumanDenoms: false,
doesAllowUnlistedDenoms: true,
mock: &mocks.TokensUsecaseMock{
IsValidChainDenomFunc: func(chainDenom string) bool {
return true
},
},
expectedDenom: ibcDenom,
expectedError: "",
},
{
name: "Invalid chain denom with unlisted allowed",
denom: "invalid",
isHumanDenoms: false,
doesAllowUnlistedDenoms: true,
mock: &mocks.TokensUsecaseMock{
IsValidChainDenomFunc: func(chainDenom string) bool {
return false
},
},
expectedDenom: "",
expectedError: "denom is not a valid chain denom (invalid)",
},
{
name: "Unlisted chain denom when not allowed",
denom: ibcDenom,
isHumanDenoms: false,
doesAllowUnlistedDenoms: false,
mock: &mocks.TokensUsecaseMock{
IsValidListedDenomFunc: func(denom string) bool {
return false
},
},
expectedDenom: "",
expectedError: "denom is not a valid listed chain denom (" + ibcDenom + ")",
},
{
name: "Valid listed chain denom",
denom: ibcDenom,
isHumanDenoms: false,
doesAllowUnlistedDenoms: false,
mock: &mocks.TokensUsecaseMock{
IsValidListedDenomFunc: func(denom string) bool {
return true
},
},
expectedDenom: ibcDenom,
expectedError: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// System under test
result, err := mvc.ValidateChainDenomQueryParam(tt.mock, tt.denom, tt.isHumanDenoms, tt.doesAllowUnlistedDenoms)

// Assert results
if tt.baseError {
assert.Empty(t, result)
assert.NoError(t, err)
} else if tt.expectedError != "" {
assert.Empty(t, result)
assert.EqualError(t, err, tt.expectedError)
} else {
assert.Equal(t, tt.expectedDenom, result)
assert.NoError(t, err)
}
})
}
}
14 changes: 11 additions & 3 deletions router/delivery/http/router_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ func (a *RouterHandler) GetOptimalQuote(c echo.Context) (err error) {
tokenIn, tokenOutDenom = req.TokenOut, req.TokenInDenom
}

chainDenoms, err := mvc.ValidateChainDenomsQueryParam(c, a.TUsecase, []string{tokenIn.Denom, tokenOutDenom})
// We do not allow unlisted denoms since this is a production from the swap tool.
doesAllowUnlistedDenoms := false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow client to specify doesAllowUnlistedDenoms param? Would defining AllowUnlistedDenoms field in types.GetQuoteRequest and setting default ( during Unmarsal ) make more sense and would allow to keep handlers smaller?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm not sure - we don't have a product use case for this.

chainDenoms, err := mvc.ValidateChainDenomsQueryParam(c, a.TUsecase, []string{tokenIn.Denom, tokenOutDenom}, doesAllowUnlistedDenoms)
if err != nil {
return c.JSON(http.StatusBadRequest, domain.ResponseError{Message: err.Error()})
}
Expand Down Expand Up @@ -200,7 +202,10 @@ func (a *RouterHandler) GetDirectCustomQuote(c echo.Context) (err error) {
}

// Apply human denoms conversion if required.
chainDenoms, err := mvc.ValidateChainDenomsQueryParam(c, a.TUsecase, append([]string{tokenIn.Denom}, tokenOutDenom...))
// We allow unlisted denoms since this is a production endpoint from the pools page
// where we want to support all chain denoms.
doesAllowUnlistedDenoms := true
chainDenoms, err := mvc.ValidateChainDenomsQueryParam(c, a.TUsecase, append([]string{tokenIn.Denom}, tokenOutDenom...), doesAllowUnlistedDenoms)
if err != nil {
return c.JSON(http.StatusBadRequest, domain.ResponseError{Message: err.Error()})
}
Expand Down Expand Up @@ -250,7 +255,10 @@ func (a *RouterHandler) GetCandidateRoutes(c echo.Context) error {
return c.JSON(domain.GetStatusCode(err), domain.ResponseError{Message: err.Error()})
}

chainDenoms, err := mvc.ValidateChainDenomsQueryParam(c, a.TUsecase, []string{tokenIn, tokenOutDenom})
// We do not allow unlisted denoms to maintain
// compatibility with the /quote endpoint.
doesAllowUnlistedDenoms := false
chainDenoms, err := mvc.ValidateChainDenomsQueryParam(c, a.TUsecase, []string{tokenIn, tokenOutDenom}, doesAllowUnlistedDenoms)
if err != nil {
return c.JSON(domain.GetStatusCode(err), domain.ResponseError{Message: err.Error()})
}
Expand Down
4 changes: 3 additions & 1 deletion tokens/delivery/http/tokens_delivery.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ func (a *TokensHandler) GetPoolDenomMetadata(c echo.Context) (err error) {

denoms := strings.Split(denomsStr, ",")
// Validate denom parameters and convert to chain denoms if necessary.
chainDenoms, err := mvc.ValidateChainDenomsQueryParam(c, a.TUsecase, denoms)
// We allow unlisted denoms since this is a debug endpoint.
doesAllowUnlistedDenoms := true
chainDenoms, err := mvc.ValidateChainDenomsQueryParam(c, a.TUsecase, denoms, doesAllowUnlistedDenoms)
Comment on lines +144 to +146
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Based on the gathered information, I can now provide the final response:

Review of doesAllowUnlistedDenoms usage shows consistent implementation

The codebase shows consistent and well-documented usage of ValidateChainDenomsQueryParam across different endpoints:

  • Debug endpoints explicitly allow unlisted denoms (doesAllowUnlistedDenoms = true)
  • Production/swap endpoints restrict to listed denoms (doesAllowUnlistedDenoms = false)
  • Each usage has clear comments explaining the reasoning

However, there's a lack of direct test coverage for ValidateChainDenomsQueryParam in domain/mvc/tokens_test.go, which should be addressed.

🔗 Analysis chain

Verify test coverage and usage consistency.

Let's verify the test coverage and usage consistency for this change.


Let me generate additional verification scripts to check for test coverage since the previous fd command had an error.


Let me check the test coverage in the mvc package since we found a test file there.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check test coverage and other usages of ValidateChainDenomsQueryParam

# Test 1: Check for test files covering this endpoint
echo "Checking for test files..."
fd -e go -e _test.go tokens/delivery/http

# Test 2: Find other usages of ValidateChainDenomsQueryParam
echo "Checking other usages of ValidateChainDenomsQueryParam..."
rg "ValidateChainDenomsQueryParam" -A 3 -B 3

Length of output: 3786


Script:

#!/bin/bash
# Check for test files and test coverage
echo "Checking for test files in tokens/delivery/http..."
fd -e go . --full-path "tokens/delivery/http" | grep "_test.go"

# Check implementation of ValidateChainDenomsQueryParam
echo "Checking implementation of ValidateChainDenomsQueryParam..."
ast-grep --pattern 'func ValidateChainDenomsQueryParam($$$) {
  $$$
}'

# Check for test files covering mvc package
echo "Checking for test files in domain/mvc..."
fd -e go . --full-path "domain/mvc" | grep "_test.go"

Length of output: 543


Script:

#!/bin/bash
# Check test coverage for ValidateChainDenomsQueryParam
echo "Checking test coverage in tokens_test.go..."
rg "TestValidateChainDenomsQueryParam|ValidateChainDenomsQueryParam.*test" -A 10 domain/mvc/tokens_test.go

# Check the actual implementation
echo "Checking implementation details..."
rg "func ValidateChainDenomsQueryParam" -A 15 domain/mvc/tokens.go

Length of output: 888

if err != nil {
return c.JSON(http.StatusBadRequest, domain.ResponseError{Message: err.Error()})
}
Expand Down
29 changes: 17 additions & 12 deletions tokens/usecase/tokens_usecase.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,18 +406,8 @@ func (t *tokensUseCase) RegisterPricingStrategy(source domain.PricingSourceType,

// IsValidChainDenom implements mvc.TokensUsecase.
func (t *tokensUseCase) IsValidChainDenom(chainDenom string) bool {
metaData, ok := t.tokenMetadataByChainDenom.Load(chainDenom)
if !ok {
return false
}

v, ok := metaData.(domain.Token)
if !ok {
return false
}

// is valid only if token is found and is not unlisted
return !v.IsUnlisted
_, ok := t.tokenMetadataByChainDenom.Load(chainDenom)
return ok
}

// GetMinPoolLiquidityCap implements mvc.TokensUsecase.
Expand Down Expand Up @@ -466,3 +456,18 @@ func (t *tokensUseCase) GetCoingeckoIdByChainDenom(chainDenom string) (string, e

return v, nil
}

// IsValidListedDenom implements mvc.TokensUsecase.
func (t *tokensUseCase) IsValidListedDenom(denom string) bool {
metaData, ok := t.tokenMetadataByChainDenom.Load(denom)
if !ok {
return false
}

v, ok := metaData.(domain.Token)
if !ok {
return false
}

return !v.IsUnlisted
}
Comment on lines +460 to +473
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider optimizing the implementation

The implementation is functionally correct but could be more concise. Consider this refactoring:

func (t *tokensUseCase) IsValidListedDenom(denom string) bool {
-	metaData, ok := t.tokenMetadataByChainDenom.Load(denom)
-	if !ok {
-		return false
-	}
-
-	v, ok := metaData.(domain.Token)
-	if !ok {
-		return false
-	}
-
-	return !v.IsUnlisted
+	if metaData, ok := t.tokenMetadataByChainDenom.Load(denom); ok {
+		if v, ok := metaData.(domain.Token); ok {
+			return !v.IsUnlisted
+		}
+	}
+	return false
}

Additionally, consider adding debug logging for failure cases to aid in troubleshooting:

func (t *tokensUseCase) IsValidListedDenom(denom string) bool {
	if metaData, ok := t.tokenMetadataByChainDenom.Load(denom); ok {
		if v, ok := metaData.(domain.Token); ok {
			return !v.IsUnlisted
		}
+		t.logger.Debug("Invalid type assertion for token metadata", zap.String("denom", denom))
	}
+	t.logger.Debug("Denom not found in metadata", zap.String("denom", denom))
	return false
}

Committable suggestion was skipped due to low confidence.

Loading
Loading