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

Feat: in given out for /custom-direct-quote #423

Merged
merged 14 commits into from
Aug 17, 2024
Merged
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
225 changes: 188 additions & 37 deletions tests/quote.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import time
from typing import Callable, Any
import conftest
from sqs_service import *
from quote_response import *
Expand All @@ -9,6 +11,34 @@
from route import *

class Quote:
@staticmethod
def run_quote_test(service_call: Callable[[], Any], expected_latency_upper_bound_ms, expected_status_code=200) -> QuoteExactAmountOutResponse:
"""
Runs exact amount out test for the /router/quote endpoint with the given input parameters.

Does basic validation around response status code and latency.

Returns quote for additional validation if needed by client.

Validates:
- Response status code is as given or default 200.
- Latency is under the given bound.
"""

start_time = time.time()
response = service_call()
elapsed_time_ms = (time.time() - start_time) * 1000

assert response.status_code == expected_status_code, f"Error: {response.text}"
assert expected_latency_upper_bound_ms > elapsed_time_ms, f"Error: latency {elapsed_time_ms} exceeded {expected_latency_upper_bound_ms} ms, denom in and token out"

response_json = response.json()

print(response.text)

# Return the response for further processing
return response.json()

@staticmethod
def choose_error_tolerance(amount: int):
# This is the max error tolerance of 7% that we allow.
Expand Down Expand Up @@ -70,6 +100,126 @@ def validate_pool_denoms_in_route(token_in_denom, token_out_denom, denoms, pool_
assert token_in_denom, f"Error: token in {token_in_denom} not found in pool denoms {denoms}, pool ID {pool_id}, route in {route_denom_in}, route out {route_denom_out}"

class ExactAmountInQuote:
@staticmethod
def calculate_expected_base_out_quote_spot_price(denom_out, coin):
"""
Compute expected base out quote spot price

First, get the USD price of each denom, and then divide to get the expected spot price
"""

# Compute expected base out quote spot price
# First, get the USD price of each denom, and then divide to get the expected spot price
in_base_usd_quote_price = conftest.get_usd_price_scaled(denom_out)
out_base_usd_quote_price = conftest.get_usd_price_scaled(coin.denom)
expected_in_base_out_quote_price = out_base_usd_quote_price / in_base_usd_quote_price

# Compute expected token out
expected_token_in = int(coin.amount) * expected_in_base_out_quote_price

token_in_amount_usdc_value = in_base_usd_quote_price * coin.amount

return expected_in_base_out_quote_price, expected_token_in, token_in_amount_usdc_value

def run_quote_test(environment_url, token_in, token_out, human_denoms, single_route, expected_latency_upper_bound_ms, expected_status_code=200) -> QuoteExactAmountInResponse:
"""
Runs a test for the /router/quote endpoint with the given input parameters.

Does basic validation around response status code and latency

Returns quote for additional validation if needed by client

Validates:
- Response status code is as given or default 200
- Latency is under the given bound
"""

service_call = lambda: conftest.SERVICE_MAP[environment_url].get_exact_amount_in_quote(token_in, token_out, human_denoms, single_route)

response = Quote.run_quote_test(service_call, expected_latency_upper_bound_ms, expected_status_code)

# Return route for more detailed validation
return QuoteExactAmountInResponse(**response)

@staticmethod
def validate_quote_test(quote, expected_amount_in_str, expected_denom_in, spot_price_scaling_factor, expected_in_base_out_quote_price, expected_token_out, denom_out, error_tolerance, direct_quote=False):
"""
Runs the following validations:
- Basic presence of fields
- Transmuter has no price impact. Otherwise, it is negative.
- Token out amount is within error tolerance from expected.
- Returned spot price is within error tolerance from expected.
"""

# Validate routes are generally present
assert len(quote.route) > 0

# Check if the route is a single pool single transmuter route
# For such routes, the price impact is 0.
is_transmuter_route = Quote.is_transmuter_in_single_route(quote.route)

# Validate price impact
# If it is a single pool single transmuter route, we expect the price impact to be 0
# Price impact is returned as a negative number for any other route.
assert quote.price_impact is not None
assert (not is_transmuter_route) and (quote.price_impact < 0) or (is_transmuter_route) and (quote.price_impact == 0), f"Error: price impact {quote.price_impact} is zero for non-transmuter route"
price_impact_positive = quote.price_impact * -1

# Validate amount in and denom are as input
assert quote.amount_in.amount == int(expected_amount_in_str)
assert quote.amount_in.denom == expected_denom_in

# Validate that the fee is charged
ExactAmountInQuote.validate_fee(quote)

# Validate that the route is valid
ExactAmountInQuote.validate_route(quote, expected_denom_in, denom_out, direct_quote)

# Validate that the spot price is present
assert quote.in_base_out_quote_spot_price is not None

# Validate that the spot price is within the error tolerance
assert relative_error(quote.in_base_out_quote_spot_price * spot_price_scaling_factor, expected_in_base_out_quote_price) < error_tolerance, f"Error: in base out quote spot price {quote.in_base_out_quote_spot_price} is not within {error_tolerance} of expected {expected_in_base_out_quote_price}"

# If there is a price impact greater than the provided error tolerance, we dynamically set the error tolerance to be
# the price impact * (1 + error_tolerance) to account for the price impact
if price_impact_positive > error_tolerance:
error_tolerance = price_impact_positive * Decimal(1 + error_tolerance)

# Validate that the amount out is within the error tolerance
amount_out_scaled = quote.amount_out * spot_price_scaling_factor
assert relative_error(amount_out_scaled, expected_token_out) < error_tolerance, f"Error: amount out scaled {amount_out_scaled} is not within {error_tolerance} of expected {expected_token_out}"

@staticmethod
def validate_route(quote, denom_in, denom_out, direct_quote=False):
"""
Validates that the route is valid by checking the following:
- The input token is present in each pool denoms
- The last token out is equal to denom out
"""
for route in quote.route:
cur_token_in_denom = denom_in
for p in route.pools:
pool_id = p.id
pool = conftest.shared_test_state.pool_by_id_map.get(str(pool_id))

assert pool, f"Error: pool ID {pool_id} not found in test data"

denoms = conftest.get_denoms_from_pool_tokens(pool.get("pool_tokens"))

# Validate route denoms are present in pool
Quote.validate_pool_denoms_in_route(cur_token_in_denom, p.token_out_denom, denoms, pool_id, denom_in, denom_out)

cur_token_in_denom = p.token_out_denom

if not direct_quote:
# Last route token out must be equal to denom out
assert denom_out == get_last_route_token_out(route), f"Error: denom out {denom_out} not equal to last token out {get_last_route_token_out(route)}"

if direct_quote:
# For direct custom quotes response always is multi route
assert denom_out == get_last_quote_route_token_out(quote), f"Error: denom out {denom_out} not equal to last token out {get_last_quote_route_token_out(quote)}"

@staticmethod
def validate_fee(quote):
"""
Expand Down Expand Up @@ -97,51 +247,47 @@ def validate_fee(quote):

class ExactAmountOutQuote:
@staticmethod
def calculate_amount_transmuter(token_out: Coin, denom_in):
# This is the max error tolerance of 5% that we allow.
# Arbitrarily hand-picked to avoid flakiness.
error_tolerance = 0.05
def calculate_expected_base_out_quote_spot_price(denom_in, coin):
"""
Compute expected base out quote spot price

# Get denom in precision.
denom_out_precision = conftest.get_denom_exponent(token_out.denom)
First, get the USD price of each denom, and then divide to get the expected spot price
"""

# Get denom out data to retrieve precision and price
denom_in_data = conftest.shared_test_state.chain_denom_to_data_map.get(denom_in)
denom_in_precision = denom_in_data.get("exponent")
in_base_usd_quote_price = conftest.get_usd_price_scaled(denom_in)
out_base_usd_quote_price = conftest.get_usd_price_scaled(coin.denom)
expected_in_base_out_quote_price = out_base_usd_quote_price / in_base_usd_quote_price

# Compute spot price scaling factor.
spot_price_scaling_factor = Decimal(10)**denom_out_precision / Decimal(10)**denom_in_precision
# Compute expected token out
expected_token_in = int(coin.amount) * expected_in_base_out_quote_price

# Compute expected spot prices
out_base_in_quote_price = Decimal(denom_in_data.get("price"))
expected_in_base_out_quote_price = 1 / out_base_in_quote_price
token_out_amount_usdc_value = in_base_usd_quote_price * coin.amount

# Compute expected token in
expected_token_in = int(token_out.amount) * expected_in_base_out_quote_price
return expected_in_base_out_quote_price, expected_token_in, token_out_amount_usdc_value

return spot_price_scaling_factor, expected_token_in, error_tolerance
@staticmethod
def run_quote_test(environment_url, token_out, token_in, human_denoms, single_route, expected_latency_upper_bound_ms, expected_status_code=200) -> QuoteExactAmountOutResponse:
"""
Runs exact amount out test for the /router/quote endpoint with the given input parameters.

def calculate_amount(tokenOut: Coin, denom_in):
# All tokens have the same default exponent, resulting in scaling factor of 1.
spot_price_scaling_factor = 1
Does basic validation around response status code and latency

token_out_denom = tokenOut.denom
amount_str = tokenOut.amount
amount_out = int(amount_str)
Returns quote for additional validation if needed by client

# Compute expected base out quote spot price
# First, get the USD price of each denom, and then divide to get the expected spot price
in_base_usd_quote_price = conftest.get_usd_price_scaled(denom_in)
out_base_usd_quote_price = conftest.get_usd_price_scaled(token_out_denom)
expected_in_base_out_quote_price = out_base_usd_quote_price / in_base_usd_quote_price
Validates:
- Response status code is as given or default 200
- Latency is under the given bound
"""

# Compute expected token out
expected_token_in = int(amount_str) * expected_in_base_out_quote_price
service_call = lambda: conftest.SERVICE_MAP[environment_url].get_exact_amount_out_quote(token_out, token_in, human_denoms, single_route)

response = Quote.run_quote_test(service_call, expected_latency_upper_bound_ms, expected_status_code)

return in_base_usd_quote_price * amount_out
# Return route for more detailed validation
return QuoteExactAmountOutResponse(**response)

@staticmethod
def validate_quote_test(quote, expected_amount_out_str, expected_denom_out, spot_price_scaling_factor, expected_in_base_out_quote_price, expected_token_in, denom_in, error_tolerance):
def validate_quote_test(quote, expected_amount_out_str, expected_denom_out, spot_price_scaling_factor, expected_in_base_out_quote_price, expected_token_in, denom_in, error_tolerance, direct_quote=False):
"""
Runs the following validations:
- Basic presence of fields
Expand Down Expand Up @@ -172,7 +318,7 @@ def validate_quote_test(quote, expected_amount_out_str, expected_denom_out, spot
ExactAmountOutQuote.validate_fee(quote)

# Validate that the route is valid
ExactAmountOutQuote.validate_route(quote, denom_in, expected_denom_out,)
ExactAmountOutQuote.validate_route(quote, denom_in, expected_denom_out, direct_quote)

# Validate that the spot price is present
assert quote.in_base_out_quote_spot_price is not None
Expand All @@ -187,10 +333,10 @@ def validate_quote_test(quote, expected_amount_out_str, expected_denom_out, spot

# Validate that the amount out is within the error tolerance
amount_in_scaled = quote.amount_in * spot_price_scaling_factor
assert relative_error(amount_in_scaled, expected_token_in) < error_tolerance, f"Error: amount out scaled {amount_in_scaled} is not within {error_tolerance} of expected {expected_token_out}"
assert relative_error(amount_in_scaled, expected_token_in) < error_tolerance, f"Error: amount out scaled {amount_in_scaled} is not within {error_tolerance} of expected {expected_token_in}"

@staticmethod
def validate_route(quote, denom_in, denom_out):
def validate_route(quote, denom_in, denom_out, direct_quote=False):
"""
Validates that the route is valid by checking the following:
- The output token is present in each pool denoms
Expand All @@ -210,8 +356,13 @@ def validate_route(quote, denom_in, denom_out):

cur_out_denom = p.token_in_denom

# Last route token in must be equal to denom in
assert denom_in == get_last_route_token_in(route), f"Error: denom in {denom_in} not equal to last token in {get_last_route_token_in(route)}"
if not direct_quote:
# Last route token in must be equal to denom in
assert denom_in == get_last_route_token_in(route), f"Error: denom in {denom_in} not equal to last token in {get_last_route_token_in(route)}"

if direct_quote:
# For direct custom quotes response always is multi route
assert denom_in == get_last_quote_route_token_in(quote), f"Error: denom in {denom_in} not equal to last token in {get_last_quote_route_token_in(quote)}"

@staticmethod
def validate_fee(quote):
Expand Down
28 changes: 28 additions & 0 deletions tests/quote_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ def __init__(self, amount_in, amount_out, route, effective_fee, price_impact, in
self.price_impact = Decimal(price_impact)
self.in_base_out_quote_spot_price = Decimal(in_base_out_quote_spot_price)

def get_pool_ids(self):
pool_ids = []
for route in self.route:
for pool in route.pools:
pool_ids.append(pool.id)
return pool_ids

def get_token_out_denoms(self):
token_out_denoms = []
for route in self.route:
for pool in route.pools:
token_out_denoms.append(pool.token_out_denom)
return token_out_denoms

# QuoteExactAmountOutResponse represents the response format
# of the /router/quote endpoint for Exact Amount Out Quote.
class QuoteExactAmountOutResponse:
Expand All @@ -51,3 +65,17 @@ def __init__(self, amount_in, amount_out, route, effective_fee, price_impact, in
self.effective_fee = Decimal(effective_fee)
self.price_impact = Decimal(price_impact)
self.in_base_out_quote_spot_price = Decimal(in_base_out_quote_spot_price)

def get_pool_ids(self):
pool_ids = []
for route in self.route:
for pool in route.pools:
pool_ids.append(pool.id)
return pool_ids

def get_token_in_denoms(self):
token_in_denoms = []
for route in self.route:
for pool in route.pools:
token_in_denoms.append(pool.token_in_denom)
return token_in_denoms
14 changes: 14 additions & 0 deletions tests/route.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,17 @@ def get_last_route_token_in(route):
for pool in route.pools:
token_in_denom = pool.token_in_denom
return token_in_denom

def get_last_quote_route_token_in(quote):
token_in_denom = ""
for route in quote.route:
for pool in route.pools:
token_in_denom = pool.token_in_denom
return token_in_denom

def get_last_quote_route_token_out(quote):
token_out_denom = ""
for route in quote.route:
for pool in route.pools:
token_out_denom = pool.token_out_denom
return token_out_denom
28 changes: 27 additions & 1 deletion tests/sqs_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def get_exact_amount_out_quote(self, token_out, denom_in, human_denoms="false",
# Send the GET request
return requests.get(self.url + ROUTER_QUOTE_URL, params=params, headers=self.headers)

def get_custom_direct_quote(self, denom_in, denom_out, pool_id):
def get_exact_amount_in_custom_direct_quote(self, denom_in, denom_out, pool_id):
"""
Fetches custom direct quote from the specified endpoint and returns it.

Expand All @@ -148,6 +148,32 @@ def get_custom_direct_quote(self, denom_in, denom_out, pool_id):
"tokenOutDenom": denom_out,
"poolID": pool_id,
}

print(params)

return requests.get(
self.url + ROUTER_CUSTOM_DIRECT_QUOTE_URL,
params=params,
headers=self.headers,
)

def get_exact_amount_out_custom_direct_quote(self, token_out, denom_in, pool_id):
"""
Fetches custom direct quote from the specified endpoint and returns it.

Similar to get_quote, instead of path finding, specific pool is enforced.

Raises error if non-200 is returned from the endpoint.
"""

params = {
"tokenOut": token_out,
"tokenInDenom": denom_in,
"poolID": pool_id,
}

print(params)

return requests.get(
self.url + ROUTER_CUSTOM_DIRECT_QUOTE_URL,
params=params,
Expand Down
Loading
Loading