From 227812ab9ffcaca308635e11e5612f6da3e0fecf Mon Sep 17 00:00:00 2001 From: Felix Henneke Date: Tue, 19 Dec 2023 12:38:01 +0100 Subject: [PATCH] bare bones server - not type checking - no utility functions --- .pylintrc | 5 +- Dockerfile | 6 +- LICENSE | 2 +- README.md | 78 +----- mypy.ini | 13 +- requirements.txt | 51 +++- src/_server.py | 114 ++------- src/models/batch_auction.py | 289 ----------------------- src/models/exchange_rate.py | 123 ---------- src/models/order.py | 369 ----------------------------- src/models/solver_args.py | 43 ---- src/models/token.py | 404 -------------------------------- src/models/types.py | 5 - src/models/uniswap.py | 322 ------------------------- src/util/constants.py | 18 -- src/util/enums.py | 37 --- src/util/exec_plan_coords.py | 21 -- src/util/numbers.py | 10 - src/util/schema.py | 399 ------------------------------- {src/util => tests}/__init__.py | 0 tests/test_endpoints.py | 158 +++++++++++++ tests/unit/test_order.py | 98 -------- 22 files changed, 243 insertions(+), 2322 deletions(-) delete mode 100644 src/models/batch_auction.py delete mode 100644 src/models/exchange_rate.py delete mode 100644 src/models/order.py delete mode 100644 src/models/solver_args.py delete mode 100644 src/models/token.py delete mode 100644 src/models/types.py delete mode 100644 src/models/uniswap.py delete mode 100644 src/util/constants.py delete mode 100644 src/util/enums.py delete mode 100644 src/util/exec_plan_coords.py delete mode 100644 src/util/numbers.py delete mode 100644 src/util/schema.py rename {src/util => tests}/__init__.py (100%) create mode 100644 tests/test_endpoints.py delete mode 100644 tests/unit/test_order.py diff --git a/.pylintrc b/.pylintrc index 70a75fc..3309ac5 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,4 +1,3 @@ -[MASTER] -disable=fixme,too-few-public-methods,too-many-instance-attributes,too-many-arguments,logging-fstring-interpolation,too-many-locals,duplicate-code, def buy_amount(self) -> Decimal: +[MAIN] -extension-pkg-allow-list=pydantic +disable=logging-fstring-interpolation diff --git a/Dockerfile b/Dockerfile index 8c1ab2d..c724656 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,4 @@ -FROM python:3.10-alpine - -RUN apk add --update gcc libc-dev linux-headers - -WORKDIR /app +FROM python:3.11-alpine # First copy over the requirements.txt and install dependencies, this makes # building subsequent images easier. diff --git a/LICENSE b/LICENSE index 2bbbd2a..ad44620 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 CoW Protocol +Copyright (c) 2022-2023 CoW Protocol Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b6ed5d0..3e543c3 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,18 @@ git clone git@github.com:cowprotocol/solver-template-py.git ## Install Requirements -1. Python 3.10 (or probably also 3.9) -2. Rust v1.60.0 or Docker +1. Python 3.11 or Docker (for running the solver) +2. Rust v1.60.0 or Docker (for running the autopilot and driver) REMOVE DRIVER FROM THIS TEMPLATE. THIS SHOULD BE EXPLAINED IN SOME TUTORIAL and not be part of this solver template ```sh -python3.10 -m venv venv +python3 -m venv venv source ./venv/bin/activate pip install -r requirements.txt ``` # Run Solver Server -```shell +```sh python -m src._server ``` @@ -32,79 +32,27 @@ docker run -p 8000:8000 gchr.io/cowprotocol/solver-template-py or build your own docker image with ```sh -docker build -t test-solver-image . +docker build -t solver-template-py . +``` + +and run it with +```sh +docker run -p 8000:8000 solver-template-py ``` # Feed an Auction Instance to the Solver -```shell +```sh curl -X POST "http://127.0.0.1:8000/solve" \ -H "accept: application/json" \ -H "Content-Type: application/json" \ --data "@data/small_example.json" ``` -# Connect to the orderbook: - -Run the driver (auction dispatcher in DryRun mode). Configured to read the orderbook -from our staging environment on Gnosis Chain. These parameters can be altered -in [.env](.env) - -## With Docker - -If you have docker installed then you can run this. - -```shell -docker run -it --rm --env-file .env --add-host host.docker.internal:host-gateway ghcr.io/cowprotocol/services solver -``` - -or without an env file (as described in -the [How to Write a Solver Tutorial](https://docs.cow.fi/tutorials/how-to-write-a-solver)) - -```shell -docker run -it --rm --add-host host.docker.internal:host-gateway ghcr.io/cowprotocol/services solver \ ---orderbook-url https://barn.api.cow.fi/xdai/api \ ---base-tokens 0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83 \ ---node-url "https://rpc.gnosischain.com" \ ---cow-dex-ag-solver-url "http://127.0.0.1:8000" \ ---solver-account 0x7942a2b3540d1ec40b2740896f87aecb2a588731 \ ---solvers CowDexAg \ ---transaction-strategy DryRun -``` - -Here we have used the orderbook-url for our staging environment on Gnosis Chain (very low traffic) so you can work with your own orders. A complete list of orderbook URLs can be found in a table at the bottom of the services repo [README](https://github.com/cowprotocol/services#solvers) - -## Without Docker - -Clone the services project with - -```shell -git clone https://github.com/cowprotocol/services.git -``` - -```shell -cargo run -p solver -- \ - --orderbook-url https://barn.api.cow.fi/xdai/api \ - --base-tokens 0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83 \ - --node-url "https://rpc.gnosischain.com" \ - --cow-dex-ag-solver-url "http://127.0.0.1:8000" \ - --solver-account 0x7942a2b3540d1ec40b2740896f87aecb2a588731 \ - --solvers CowDexAg \ - --transaction-strategy DryRun \ - --log-filter=info,solver=debug -``` - -# Place an order - -Navigate to [barn.cowswap.exchange/](https://barn.cowswap.exchange/#/swap) and place a -tiny (real) order. See your driver pick it up and include it in the next auction being -sent to your solver +# Connect to the orderbook TBD: -# References +# References TBD - How to Build a Solver: https://docs.cow.fi/tutorials/how-to-write-a-solver - In Depth Solver Specification: https://docs.cow.fi/off-chain-services/in-depth-solver-specification -- Settlement Contract (namely the settle - method): https://github.com/cowprotocol/contracts/blob/ff6fb7cad7787b8d43a6468809cacb799601a10e/src/contracts/GPv2Settlement.sol#L121-L143 -- Interaction Model (Currently missing from this framework): https://github.com/cowprotocol/services/blob/cda5e36db34c55e7bf9eb4ea8b6e36ecb046f2b2/crates/shared/src/http_solver/model.rs#L125-L130 diff --git a/mypy.ini b/mypy.ini index eefb839..797933d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,13 +1,2 @@ [mypy] -python_version = 3.10 - - -[mypy-src.*] -allow_untyped_calls = True -allow_any_generics = True - -[mypy-uvicorn.*] -ignore_missing_imports = True - -[mypy-fastapi] -implicit_reexport = True \ No newline at end of file +python_version = 3.11 diff --git a/requirements.txt b/requirements.txt index c439d6e..b9aaa0c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,42 @@ -black==22.3.0 -fastapi==0.65.2 -uvicorn==0.17.6 -pylint==2.13.5 -python-dotenv==0.20.0 -pydantic==1.9.0 -pytest==7.1.1 -makefun==1.13.1 -mypy==0.942 +aiohttp==3.9.1 +aiosignal==1.3.1 +annotated-types==0.6.0 +anyio==3.7.1 +astroid==3.0.2 +attrs==23.1.0 +black==23.12.0 +certifi==2023.11.17 +click==8.1.7 +dill==0.3.7 +fastapi==0.105.0 +frozenlist==1.4.1 +h11==0.14.0 +httpcore==1.0.2 +httptools==0.6.1 +httpx==0.25.2 +idna==3.6 +iniconfig==2.0.0 +isort==5.13.2 +mccabe==0.7.0 +multidict==6.0.4 +mypy==1.7.1 +mypy-extensions==1.0.0 +packaging==23.2 +pathspec==0.12.1 +platformdirs==4.1.0 +pluggy==1.3.0 +pydantic==2.5.2 +pydantic_core==2.14.5 +pylint==3.0.3 +pytest==7.4.3 +python-dotenv==1.0.0 +PyYAML==6.0.1 +sniffio==1.3.0 +starlette==0.27.0 +tomlkit==0.12.3 +typing_extensions==4.9.0 +uvicorn==0.24.0.post1 +uvloop==0.19.0 +watchfiles==0.21.0 +websockets==12.0 +yarl==1.9.4 diff --git a/src/_server.py b/src/_server.py index 61d9c4b..3c6f4b8 100644 --- a/src/_server.py +++ b/src/_server.py @@ -1,124 +1,60 @@ """ This is the project's Entry point. """ -from __future__ import annotations -import argparse -import decimal import logging - +from typing import Any import uvicorn -from dotenv import load_dotenv -from fastapi import FastAPI, Request -from fastapi.middleware.gzip import GZipMiddleware -from pydantic import BaseSettings - -from src.models.batch_auction import BatchAuction -from src.models.solver_args import SolverArgs -from src.util.schema import ( - BatchAuctionModel, - SettledBatchAuctionModel, -) - -# Set decimal precision. -decimal.getcontext().prec = 100 - -# Holds parameters passed on the command line when invoking the server. -# These will be merged with request solver parameters -SERVER_ARGS = None - +from fastapi import FastAPI -# ++++ Interface definition ++++ +# from src.models.solve_model import Auction, Solution - -# Server settings: Can be overridden by passing them as env vars or in a .env file. -# Example: PORT=8001 python -m src._server -class ServerSettings(BaseSettings): - """Basic Server Settings""" - - host: str = "0.0.0.0" - port: int = 8000 - - -server_settings = ServerSettings() +logging.basicConfig(level=logging.DEBUG) # ++++ Endpoints: ++++ - app = FastAPI(title="Batch auction solver") -app.add_middleware(GZipMiddleware) - - -@app.get("/health", status_code=200) -def health() -> bool: - """Convenience endpoint to check if server is alive.""" - return True @app.post("/notify", response_model=bool) -async def notify(request: Request) -> bool: +async def notify(notification: dict[str, Any]) -> bool: """Print response from notify endpoint.""" - print(f"Notify request {await request.json()}") + logging.debug(f"Notification: {notification}") return True -@app.post("/solve", response_model=SettledBatchAuctionModel) -async def solve(problem: BatchAuctionModel, request: Request): # type: ignore +# @app.post("/solve", response_model=Solution) +# async def solve(auction: Auction, request: Request): # type: ignore +@app.post("/solve") +async def solve(auction: dict[str, Any]) -> dict[str, Any]: """API POST solve endpoint handler""" - logging.debug(f"Received solve request {await request.json()}") - solver_args = SolverArgs.from_request(request=request, meta=problem.metadata) - - batch = BatchAuction.from_dict(problem.dict(), solver_args.instance_name) - - print("Received Batch Auction", batch.name) - print("Parameters Supplied", solver_args) + logging.debug(f"Received solve request: {auction}") - # 1. Solve BatchAuction: update batch_auction with - # batch.solve() + # 1. Solve Auction + # (add code) - trivial_solution = { - "orders": {}, - "foreign_liquidity_orders": [], - "amms": {}, + solution = { + "id": "123", + "trades": [], "prices": {}, - "approvals": [], - "interaction_data": [], + "interactions": [], + "solver": "solvertemplate", "score": "0", + "weth": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", } - print("\n\n*************\n\nReturning solution: " + str(trivial_solution)) - return trivial_solution + logging.debug(f"Returning solution: {solution}") + + # return Solution(**solution) + return solution # ++++ Server setup: ++++ if __name__ == "__main__": - load_dotenv() - - parser = argparse.ArgumentParser( - fromfile_prefix_chars="@", - formatter_class=argparse.ArgumentDefaultsHelpFormatter, - ) - # TODO - enable flag to write files to persistent storage - # parser.add_argument( - # "--write_auxiliary_files", - # type=bool, - # default=False, - # help="Write auxiliary instance and optimization files, or not.", - # ) - - parser.add_argument( - "--log-level", - type=str, - default="info", - help="Log level", - ) - - SERVER_ARGS = parser.parse_args() uvicorn.run( "__main__:app", - host=server_settings.host, - port=server_settings.port, - log_level=SERVER_ARGS.log_level, + host="0.0.0.0", + port=8000, ) diff --git a/src/models/batch_auction.py b/src/models/batch_auction.py deleted file mode 100644 index 0956e0f..0000000 --- a/src/models/batch_auction.py +++ /dev/null @@ -1,289 +0,0 @@ -""" -Model containing BatchAuction which is what solvers operate on. -""" - -from __future__ import annotations -import decimal -import logging -from decimal import Decimal -from typing import Any, Optional - -from src.models.order import Order, OrdersSerializedType -from src.models.token import ( - Token, - TokenInfo, - select_token_with_highest_normalize_priority, - TokenDict, - TokenSerializedType, -) -from src.models.types import NumericType -from src.models.uniswap import Uniswap, UniswapsSerializedType -from src.util.enums import Chain - - -class BatchAuction: - """Class to represent a batch auction.""" - - def __init__( - self, - tokens: dict[Token, TokenInfo], - orders: dict[str, Order], - uniswaps: dict[str, Uniswap], - ref_token: Token, - prices: Optional[dict] = None, - name: str = "batch_auction", - metadata: Optional[dict] = None, - ): - """Initialize. - Args: - tokens: dict of tokens participating. - orders: dict of Order objects. - uniswaps: dict of Uniswap objects. - ref_token: Reference Token object. - prices: A dict of {token -> price}. - name: Name of the batch auction instance. - metadata: Some instance metadata. - """ - self.name = name - self.metadata = metadata if metadata else {} - - self._tokens = tokens - self._orders = orders - self._uniswaps = uniswaps - - # Store reference token and (previous) prices. - self.ref_token = ref_token - self.prices = ( - prices if prices else {ref_token: self._tokens[ref_token].external_price} - ) - - @classmethod - def from_dict(cls, data: dict, name: str) -> BatchAuction: - """Read a batch auction instance from a python dictionary. - - Args: - data: Python dict to be read. - name: Instance name. - - Returns: - The instance. - - """ - for key in ["tokens", "orders"]: - if key not in data: - raise ValueError(f"Mandatory field '{key}' missing in instance data!") - - tokens = load_tokens(data["tokens"]) - orders = load_orders(data["orders"]) - - uniswaps = load_amms(data.get("amms", {})) - metadata = load_metadata(data.get("metadata", {})) - prices = load_prices(data.get("prices", {})) - - ref_token = select_token_with_highest_normalize_priority(tokens) - - return cls( - tokens, - orders, - uniswaps, - ref_token, - prices=prices, - metadata=metadata, - name=name, - ) - - #################### - # ACCESS METHODS # - #################### - - @property - def tokens(self) -> list[Token]: - """Access to (sorted) token list.""" - return sorted(self._tokens.keys()) - - @property - def orders(self) -> list[Order]: - """Access to order list.""" - return list(self._orders.values()) - - @property - def uniswaps(self) -> list[Uniswap]: - """Access to uniswap list.""" - return list(self._uniswaps.values()) - - def token_info(self, token: Token) -> TokenInfo: - """Get the token info for a specific token.""" - assert isinstance(token, Token) - - if token not in self.tokens: - raise ValueError(f"Token <{token}> not in batch auction!") - - return self._tokens[token] - - @property - def chain(self) -> Chain: - """Return the blockchain on which the BatchAuction happens.""" - if self.ref_token is None: - return Chain.UNKNOWN - ref = self.ref_token.value.lower() - if ref == "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": - return Chain.MAINNET - if ref == "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d": - return Chain.XDAI - return Chain.UNKNOWN - - @property - def default_ref_token_price(self) -> Decimal: - """Price of reference token if not given explicitly. - - This price is chosen so that the price of one unit of - a token with d_t=18 decimals that costs one unit of the - reference token is p_t=10^18: - - a_t * p_t * 10^d_t = a_r * p_r * 10^d_r - - with: - a_t/a_r = 1 - d_t = 18 - p_t = 10^18 - - p_r = a_t/a_r * 10^18 * 10^18 / 10^d_r - <-> p_r = 10^(2 * 18 - d_r) - """ - return Decimal(10) ** (2 * 18 - self.token_info(self.ref_token).decimals) - - def solve(self) -> None: - """Solve Batch""" - - ################################# - # SOLUTION PROCESSING METHODS # - ################################# - - def __str__(self) -> str: - """Print batch auction data. - - Returns: - The string representation. - - """ - output_str = "BATCH AUCTION:" - - output_str += f"\n=== TOKENS ({len(self.tokens)}) ===" - for token in self.tokens: - output_str += f"\n-- {token}" - - output_str += f"\n=== ORDERS ({len(self.orders)}) ===" - for order in self.orders: - output_str += f"\n{order}" - - output_str += f"\n=== UNISWAPS ({len(self.uniswaps)}) ===" - for uni in self.uniswaps: - output_str += f"\n{uni}" - - return output_str - - def __repr__(self) -> str: - """Print batch auction data.""" - return self.name - - -def load_metadata(metadata: dict[str, Any]) -> dict[str, Any]: - """Store some basic metadata information.""" - metadata["scaling_factors"] = { - Token(t): Decimal(f) for t, f in metadata.get("scaling_factors", {}).items() - } - - return metadata - - -def load_prices( - prices_serialized: dict[TokenSerializedType, NumericType] -) -> dict[Token, Decimal]: - """Load token price information as dict of Token -> Decimal.""" - if not isinstance(prices_serialized, dict): - raise ValueError( - f"Prices must be given as dict, not {type(prices_serialized)}!" - ) - return {Token(t): Decimal(p) for t, p in prices_serialized.items()} - - -def load_orders(orders_serialized: OrdersSerializedType) -> dict[str, Order]: - """Load dict of orders as order pool_id -> order data. - - Args: - orders_serialized: dict of order_id -> order data dict. - - Returns: - A list of Order objects. - """ - order_list = [ - Order.from_dict(order_id, data) for order_id, data in orders_serialized.items() - ] - result: dict[str, Order] = {} - for order in order_list: - if order.order_id in result: - raise ValueError(f"Order pool_id <{order.order_id}> already exists!") - result[order.order_id] = order - return result - - -def load_amms(amms_serialized: UniswapsSerializedType) -> dict[str, Uniswap]: - """Load list of AMMs. - - NOTE: Currently, the code only supports Uniswap-style AMMs, i.e., - constant-product pools with two tokens and equal weights. - - Args: - amms_serialized: dict of pool_id -> AMM. - - Returns: - A list of Uniswap objects. - - """ - amm_list = [] - for amm_id, amm_data in amms_serialized.items(): - amm = Uniswap.from_dict(amm_id, amm_data) - if amm is not None: - amm_list.append(amm) - - results: dict[str, Uniswap] = {} - for uni in amm_list: - if uni.pool_id in results: - raise ValueError(f"Uniswap pool_id <{uni.pool_id}> already exists!") - results[uni.pool_id] = uni - - return results - - -def load_tokens(tokens_serialized: dict) -> TokenDict: - """Store tokens as sorted dictionary from Token -> token info. - - Args: - tokens_serialized: list or dict of tokens. - - Returns: - A dict of Token -> token info. - - """ - tokens_dict = {} - for token_str, token_info in sorted(tokens_serialized.items()): - token = Token(token_str) - if token_info is None: - token_info = {} - else: - for k, val in token_info.items(): - if val is None: - continue - try: - # Convert to str first to avoid float-precision artifacts: - # Decimal(0.1) -> Decimal('0.10000000000000000555...') - # Decimal('0.1') -> Decimal(0.1) - val = Decimal(str(val)) - except decimal.InvalidOperation: - pass - token_info[k] = val - if token in tokens_dict: - logging.warning(f"Token <{token}> already exists!") - tokens_dict[token] = TokenInfo(token, **token_info) - - return tokens_dict diff --git a/src/models/exchange_rate.py b/src/models/exchange_rate.py deleted file mode 100644 index 3694d4a..0000000 --- a/src/models/exchange_rate.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Representation of an exchange rate between two tokens.""" -from __future__ import annotations - -from src.models.token import Token, TokenBalance -from src.models.types import NumericType - - -class ExchangeRate: - """Class representing the exchange rate between two tokens.""" - - def __init__(self, tb1: TokenBalance, tb2: TokenBalance): - """ - An ExchangeRate is represented as equivalence of two TokenBalances, - e.g., 2 [ETH] == 800 [EUR] --> 400 [EUR]/[ETH] or 0.0025 [ETH]/[EUR]. - Args: - tb1: First TokenBalance. - tb2: Second TokenBalance. - """ - assert isinstance(tb1, TokenBalance) - assert isinstance(tb2, TokenBalance) - - if tb1.token == tb2.token: - raise ValueError("Both given tokens are identical!") - - if not (tb1.is_positive() and tb2.is_positive()): - raise ValueError(f"Both token balances must be positive! {tb1} & {tb2}") - - # Store attributes. - self.tb1 = tb1 - self.tb2 = tb2 - - @classmethod - def from_prices( - cls, - token1_price: tuple[Token, NumericType], - token2_price: tuple[Token, NumericType], - ) -> ExchangeRate: - """Alternative constructor: Build ExchangeRate from (absolute) token prices. - - Args: - token1_price: Tuple of (Token, price). - token2_price: Tuple of (Token, price). - Returns: - The ExchangeRate between the input tokens at the given prices. - """ - # Validate Input - balances = [] - for token_price in [token1_price, token2_price]: - assert isinstance(token_price, tuple) and len(token_price) == 2 - token, price = token_price - assert isinstance(token, Token) - assert price > 0 - - balances.append(TokenBalance(price, token)) - - return cls(balances[0], balances[1]) - - def token_balance(self, token: Token) -> TokenBalance: - """Get token balance for given token.""" - if token == self.tb1.token: - return self.tb1 - if token == self.tb2.token: - return self.tb2 - - raise ValueError(f"Exchange rate does not involve {token}") - - @property - def tokens(self) -> set[Token]: - """Returns a set containing the two tokens.""" - return {self.tb1.token, self.tb2.token} - - def convert(self, token_balance: TokenBalance) -> TokenBalance: - """Convert a TokenBalance of one token into a TokenBalance of the other. - Args: - token_balance: TokenBalance to be converted. - Returns: Converted TokenBalance. - """ - assert isinstance(token_balance, TokenBalance) - - if token_balance.token == self.tb1.token: - # Division of two token balances with same token yields scalar - # which can then be multiplied with another token balance. - return (token_balance / self.tb1).as_decimal() * self.tb2 - - if token_balance.token == self.tb2.token: - return (token_balance / self.tb2).as_decimal() * self.tb1 - - raise ValueError( - f"Token balance <{token_balance}> can not be " - f"converted using ExchangeRate <{self.tb1.token}/{self.tb2.token}>!" - ) - - def convert_unit(self, token: Token) -> TokenBalance: - """Convert one unit of one token into a TokenBalance of the other. - Args: - token: Token to be converted. - Returns: - Converted TokenBalance. - """ - assert isinstance(token, Token) - return self.convert(TokenBalance(1, token)) - - def __eq__(self, other: object) -> bool: - """Equality operator""" - if not isinstance(other, ExchangeRate): - raise ValueError(f"Cannot compare ExchangeRate and type <{type(other)}>!") - - # The ratio of the TokenBalances must be equal. - return self.convert(other.tb1) == other.tb2 - - def __ne__(self, other: object) -> bool: - """Non-equality operator""" - return not self == other - - def __str__(self) -> str: - """Represent as string.""" - tb1 = self.convert(TokenBalance(1, self.tb1.token)) - tb2 = self.convert(TokenBalance(1, self.tb2.token)) - return f"{tb1}/[{self.tb1.token}] <=> {tb2}/[{self.tb2.token}]" - - def __repr__(self) -> str: - """Represent as string.""" - return str(self) diff --git a/src/models/order.py b/src/models/order.py deleted file mode 100644 index fc0b607..0000000 --- a/src/models/order.py +++ /dev/null @@ -1,369 +0,0 @@ -"""Representation of a limit order.""" -from __future__ import annotations - -import json -import logging -from decimal import Decimal -from enum import Enum -from typing import Optional, Any, Union - - -from src.models.exchange_rate import ExchangeRate as XRate -from src.models.token import Token, TokenBalance -from src.models.types import NumericType -from src.util.constants import Constants -from src.util.numbers import decimal_to_str - -OrderSerializedType = dict[str, Any] -OrdersSerializedType = dict[str, OrderSerializedType] - - -class OrderMatchType(Enum): - """Enum for different Order Matching""" - - LHS_FILLED = "LhsFilled" - RHS_FILLED = "RhsFilled" - BOTH_FILLED = "BothFilled" - - -# TODO - use dataclass for this. -class Order: - """Representation of a limit order. - An order is specified with 3 bounds: - * maximum amount of buy-token to be bought - * maximum amount of sell-token to be sold - * limit exchange rate of buy- vs. sell-token. - - Depending on which of the bounds are set, - the order represents a classical limit {buy|sell} order, - a cost-bounded {buy|sell} order or a {buy|sell} market order. - """ - - def __init__( - self, - order_id: str, - buy_token: Token, - sell_token: Token, - buy_amount: Decimal, - sell_amount: Decimal, - is_sell_order: bool, - allow_partial_fill: bool = False, - is_liquidity_order: bool = False, - has_atomic_execution: bool = False, - fee: Optional[TokenBalance] = None, - cost: Optional[TokenBalance] = None, - ) -> None: - """Initialize. - - Args: - order_id: Order pool_id. - buy_token: Token to be bought. - sell_token: Token to be sold. - - Kwargs: - max_buy_amount: Maximum amount of buy-token to be bought, or None. - max_sell_amount: Maximum amount of sell-token to be sold, or None. - max_limit: Limit exchange rate for order. - allow_partial_fill: Can order be partially matched, or not. - is_liquidity_order: Is the order from a market maker, or not. - has_atomic_execution: Needs to executed atomically, or not. - fee: Fee contribution of the order to the objective. - cost: Cost of including the order in the solution. - exec_buy_amount: Matched amount of buy-token in solution. - exec_sell_amount: Matched amount of sell-token in solution. - """ - if buy_token == sell_token: - raise ValueError("sell- and buy-token cannot be equal!") - - if not (buy_amount > 0 and sell_amount > 0): - raise ValueError( - f"buy {buy_amount} and sell {sell_amount} amounts must be positive!" - ) - - self.order_id = order_id - self.buy_token = buy_token - self.sell_token = sell_token - self.buy_amount = buy_amount - self.sell_amount = sell_amount - self.is_sell_order = is_sell_order - self.allow_partial_fill = allow_partial_fill - self.is_liquidity_order = is_liquidity_order - self.has_atomic_execution = has_atomic_execution - self.fee: Optional[TokenBalance] = fee - self.cost: Optional[TokenBalance] = cost - - # Stuff that isn't part of the constructor parameters. - self.exec_buy_amount: Optional[TokenBalance] = None - self.exec_sell_amount: Optional[TokenBalance] = None - - @classmethod - def from_dict(cls, order_id: str, data: OrderSerializedType) -> Order: - """ - Read Order object from order data dict. - Args: - order_id: ID of order - data: Dict of order data. - """ - - required_attributes = [ - "sell_token", - "buy_token", - "sell_amount", - "buy_amount", - "is_sell_order", - "allow_partial_fill", - "is_liquidity_order", - ] - - for attr in required_attributes: - if attr not in data: - raise ValueError(f"Missing field '{attr}' in order <{order_id}>!") - - return Order( - order_id=order_id, - buy_token=Token(data["buy_token"]), - sell_token=Token(data["sell_token"]), - buy_amount=Decimal(data["buy_amount"]), - sell_amount=Decimal(data["sell_amount"]), - is_sell_order=bool(data["is_sell_order"]), - allow_partial_fill=bool(data["allow_partial_fill"]), - is_liquidity_order=bool(data["is_liquidity_order"]), - fee=TokenBalance.parse(data.get("fee"), allow_none=True), - cost=TokenBalance.parse(data.get("cost"), allow_none=True), - ) - - def as_dict(self) -> OrderSerializedType: - """Return Order object as dictionary.""" - # Currently, only limit buy or sell orders be handled. - order_dict = { - "sell_token": str(self.sell_token), - "buy_token": str(self.buy_token), - "sell_amount": decimal_to_str(self.sell_amount), - "buy_amount": decimal_to_str(self.buy_amount), - "allow_partial_fill": self.allow_partial_fill, - "is_sell_order": self.is_sell_order, - "exec_sell_amount": decimal_to_str(self.exec_sell_amount.as_decimal()) - if self.exec_sell_amount is not None - else "0", - "exec_buy_amount": decimal_to_str(self.exec_buy_amount.as_decimal()) - if self.exec_buy_amount is not None - else "0", - } - - if self.fee is not None: - order_dict["fee"] = { - "token": str(self.fee.token), - "amount": decimal_to_str(self.fee.as_decimal()), - } - - if self.cost is not None: - order_dict["cost"] = { - "token": str(self.cost.token), - "amount": decimal_to_str(self.cost.as_decimal()), - } - - return order_dict - - @property - def max_limit(self) -> XRate: - """Max limit of the order as an exchange rate""" - return XRate( - tb1=TokenBalance(self.sell_amount, self.sell_token), - tb2=TokenBalance(self.buy_amount, self.buy_token), - ) - - @property - def max_buy_amount(self) -> Optional[TokenBalance]: - """None for sell-orders""" - if not self.is_sell_order: - return TokenBalance.parse_amount(self.buy_amount, self.buy_token) - return None - - @property - def max_sell_amount(self) -> Optional[TokenBalance]: - """None for buy-orders""" - if self.is_sell_order: - return TokenBalance.parse_amount(self.sell_amount, self.sell_token) - return None - - @property - def tokens(self) -> set[Token]: - """Return the buy and sell tokens.""" - return {self.buy_token, self.sell_token} - - ##################### - # UTILITY METHODS #` - ##################### - - def overlaps(self, other: Order) -> bool: - """ - Determine if one order can be matched with another. - opposite {buy|sell} tokens and matching prices - """ - token_conditions = [ - self.buy_token == other.sell_token, - self.sell_token == other.buy_token, - ] - if not all(token_conditions): - return False - - return ( - self.buy_amount * other.buy_amount <= other.sell_amount * self.sell_amount - ) - - def match_type(self, other: Order) -> Optional[OrderMatchType]: - """Determine to what extent two orders match""" - if not self.overlaps(other): - return None - - if self.buy_amount < other.sell_amount and self.sell_amount < other.buy_amount: - return OrderMatchType.LHS_FILLED - - if self.buy_amount > other.sell_amount and self.sell_amount > other.buy_amount: - return OrderMatchType.RHS_FILLED - - return OrderMatchType.BOTH_FILLED - - def is_executable(self, xrate: XRate, xrate_tol: Decimal = Decimal("1e-6")) -> bool: - """Determine if the order limit price satisfies a given market rate. - - Args: - xrate: Exchange rate. - xrate_tol: Accepted violation of the limit exchange rate constraint - per unit of buy token (default: 1e-6). - Returns: - True, if order can be executed; False otherwise. - """ - buy_token, sell_token = self.buy_token, self.sell_token - if xrate.tokens != {buy_token, sell_token}: - raise ValueError( - f"Exchange rate and order tokens do not " - f"match: {xrate} vs. <{buy_token}> | <{sell_token}>!" - ) - - assert xrate_tol >= 0 - converted_buy = xrate.convert_unit(buy_token) - converted_sell = self.max_limit.convert_unit(buy_token) - return bool(converted_buy <= (converted_sell * (1 + xrate_tol))) - - def execute( - self, - buy_amount_value: NumericType, - sell_amount_value: NumericType, - buy_token_price: Union[float, Decimal] = 0, - sell_token_price: Union[float, Decimal] = 0, - amount_tol: Decimal = Decimal("1e-8"), - xrate_tol: Decimal = Decimal("1e-6"), - ) -> None: - """Execute the order at given amounts. - - Args: - buy_amount_value: Buy amount. - sell_amount_value: Sell amount. - buy_token_price: Buy-token price. - sell_token_price: Sell-token price. - amount_tol: Accepted violation of the limit buy/sell amount constraints. - xrate_tol: Accepted violation of the limit exchange rate constraint - per unit of buy token (default: 1e-6). - """ - assert buy_amount_value >= -amount_tol - assert sell_amount_value >= -amount_tol - assert buy_token_price >= 0 - assert sell_token_price >= 0 - - buy_token, sell_token = self.buy_token, self.sell_token - - buy_amount = TokenBalance(buy_amount_value, buy_token) - sell_amount = TokenBalance(sell_amount_value, sell_token) - - xmax = self.max_buy_amount - ymax = self.max_sell_amount - - # (a) Check buyAmount: if too much above maxBuyAmount --> error! - if xmax is not None: - if buy_amount > xmax * ( - 1 + amount_tol - ) and buy_amount > xmax + TokenBalance(amount_tol, buy_token): - raise ValueError( - f"Invalid execution request for " - f"order <{self.order_id}>: " - f"buy amount (exec) : {buy_amount.balance} " - f"buy amount (max) : {xmax.balance}" - ) - - buy_amount = min(buy_amount, xmax) - - # (b) Check sellAmount: if too much above maxSellAmount --> error! - if ymax is not None: - if sell_amount > ymax * ( - 1 + amount_tol - ) and sell_amount > ymax + TokenBalance(amount_tol, sell_token): - message = ( - f"Invalid execution request for " - f"order <{self.order_id}>: " - f"sell (exec) : {sell_amount.balance} " - f"sell (max) : {ymax.balance}" - ) - logging.error(message) - if Constants.RAISE_ON_MAX_SELL_AMOUNT_VIOLATION: - raise ValueError(message) - sell_amount = min(sell_amount, ymax) - - # (c) if any amount is very small, set to zero. - if any( - [ - buy_amount <= TokenBalance(amount_tol, buy_token), - sell_amount <= TokenBalance(amount_tol, sell_token), - ] - ): - buy_amount = TokenBalance(0.0, buy_token) - sell_amount = TokenBalance(0.0, sell_token) - - # Check limit price. - if buy_amount > 0: - assert sell_amount > 0 - xrate = XRate(buy_amount, sell_amount) - if not self.is_executable(xrate, xrate_tol=xrate_tol): - message = ( - f"Invalid execution request for order <{self.order_id}>: " - f"buy amount (exec): {buy_amount.balance} " - f"sell amount (exec): {sell_amount.balance} " - f"xrate (exec): {xrate} " - f"limit (max): {self.max_limit}" - ) - logging.error(message) - if Constants.RAISE_ON_LIMIT_XRATE_VIOLATION: - raise ValueError(message) - - # Store execution information. - self.exec_buy_amount = buy_amount - self.exec_sell_amount = sell_amount - - def is_executed(self) -> bool: - """Check if order has already been executed.""" - return self.exec_buy_amount is not None and self.exec_sell_amount is not None - - def __str__(self) -> str: - """Represent as string.""" - return json.dumps(self.as_dict(), indent=2) - - def __repr__(self) -> str: - """Represent as short string.""" - return f"Order: {self.order_id}" - - def __hash__(self) -> int: - return hash(self.order_id) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Order): - return NotImplemented - if self.order_id != other.order_id: - return False - assert vars(self) == vars(other) - return True - - def __lt__(self, other: object) -> bool: - if not isinstance(other, Order): - return NotImplemented - - return self.order_id < other.order_id diff --git a/src/models/solver_args.py b/src/models/solver_args.py deleted file mode 100644 index f33ea9c..0000000 --- a/src/models/solver_args.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Argument parser for solve Request that combines query parameters with metadata""" -from __future__ import annotations - -from dataclasses import dataclass -from typing import Optional - -from fastapi import Request - -from src.util.schema import MetadataModel - - -@dataclass -class SolverArgs: - """Parameters passed in POST URL""" - - auction_id: Optional[str] - instance_name: str - time_limit: int - max_nr_exec_orders: int - use_internal_buffers: bool - use_external_prices: bool - environment: Optional[str] - gas_price: Optional[float] - native_token: Optional[str] - - @classmethod - def from_request(cls, request: Request, meta: MetadataModel) -> SolverArgs: - """Parses Request query params dict as struct""" - param_dict = request.query_params - return cls( - # Query Parameter Arguments - instance_name=param_dict.get("instance_name", "Not Provided"), - time_limit=int(param_dict.get("time_limit", 30)), - max_nr_exec_orders=int(param_dict.get("max_nr_exec_orders", 100)), - use_internal_buffers=bool(param_dict.get("use_internal_buffers", False)), - use_external_prices=bool(param_dict.get("use_external_prices", False)), - # Meta Data Arguments - environment=meta.environment, - gas_price=meta.gas_price, - native_token=meta.native_token, - # Both: Prioritize query params over metadata. - auction_id=param_dict.get("auction_id", meta.auction_id), - ) diff --git a/src/models/token.py b/src/models/token.py deleted file mode 100644 index 1922c71..0000000 --- a/src/models/token.py +++ /dev/null @@ -1,404 +0,0 @@ -"""A class for extendable token enum's.""" -from __future__ import annotations - -import re -from decimal import Decimal, getcontext -from typing import Optional, Union - -from src.models.types import NumericType -from src.util.constants import Constants - - -class Token: - """Enumeration over available tokens.""" - - def __init__(self, value: str): - if Token._is_valid(value): - self.value = value - else: - raise ValueError(f"Invalid Ethereum Address {value}") - - @staticmethod - def _is_valid(address: str) -> bool: - match_result = re.match( - pattern=r"^(0x)?[0-9a-f]{40}$", string=address, flags=re.IGNORECASE - ) - return match_result is not None - - def __str__(self) -> str: - """Convert to string.""" - return self.value - - def __repr__(self) -> str: - """Convert to string.""" - return self.__str__() - - def __hash__(self) -> int: - """Hash of token.""" - return hash(self.value) - - def __eq__(self, other: object) -> bool: - """Equality operator.""" - if isinstance(other, Token): - return self.value == other.value - return False - - def __lt__(self, other: object) -> bool: - """Less-than operator.""" - if isinstance(other, Token): - return self.value < other.value - return NotImplemented - - -class TokenInfo: - """Class for storing token information.""" - - def __init__( - self, - token: Token, - decimals: int, - alias: Optional[str] = None, - external_price: Optional[Decimal] = None, - estimated_price: Optional[Decimal] = None, - internal_buffer: Optional[Decimal] = None, - normalize_priority: Optional[int] = 0, - ): - """Constructor.""" - self.token = token - self.alias = alias - self.decimals = decimals - self.external_price = external_price - self.estimated_price = estimated_price - self.internal_buffer = internal_buffer - self._normalize_priority = normalize_priority or 0 - - @property - def normalize_priority(self) -> int: - """ - Return the token priority for normalization purposes. - - Higher value means higher priority. - """ - return self._normalize_priority - - def as_dict(self) -> dict: - """Convert to dict.""" - attr = [ - a - for a in dir(self) - if not callable(getattr(self, a)) and not a.startswith("_") and a != "token" - ] - - return {a: getattr(self, a) for a in sorted(attr)} - - def __str__(self) -> str: - """Convert to string.""" - token_info_dict = self.as_dict() - - _str = f"Token [{self.token}]:" - for attr, value in token_info_dict.items(): - if isinstance(value, Decimal) and attr not in [ - "external_price", - "internal_buffer", - ]: - value = value.quantize(Constants.DECIMAL_STR_PREC) - _str += f"\n-- {attr} : {value}" - - return _str - - -def select_token_with_highest_normalize_priority( - tokens: dict[Token, TokenInfo] -) -> Token: - """ - Select token with highest normalize priority from the list of tokens. - - If the highest normalize_priority is shared by multiple tokens, the - ref_token is the first lexicographically. - """ - max_priority = max(t.normalize_priority for t in tokens.values()) - highest_priority_tokens = [ - t for t, info in tokens.items() if info.normalize_priority == max_priority - ] - return highest_priority_tokens[0] - - -TokenDict = dict[Token, TokenInfo] -TokenSerializedType = str -TokenAmountSerializedType = tuple[Union[str, NumericType], TokenSerializedType] - - -class TokenBalance: - """Class to represent an amount of some token.""" - - def __init__(self, balance: NumericType, token: Token): - """Initialize. - - Args: - balance: Amount of tokens. - token: Token. - """ - - self._balance = Decimal(balance) - self.balance = balance - self.token = token - - if not self._balance.is_finite(): - raise ValueError(f"Token balance must be finite, not {self._balance}!") - - @classmethod - def parse( - cls, - token_amount_serialized: Optional[TokenAmountSerializedType], - allow_negative: bool = False, - allow_none: bool = False, - ) -> Optional[TokenBalance]: - """ - Method to parse a token amount given as (amount, token) into a TokenBalance. - """ - if token_amount_serialized is None: - if not allow_none: - raise ValueError("Token amount must not be None!") - token_amount = None - else: - if not isinstance(token_amount_serialized, dict) or set( - token_amount_serialized.keys() - ) != {"amount", "token"}: - raise ValueError( - "token amount must be given as dict of {'amount': .., 'token': ..}," - f" not <{token_amount_serialized}>!" - ) - token_amount = cls( - Decimal(token_amount_serialized["amount"]), - Token(token_amount_serialized["token"]), - ) - if not allow_negative and token_amount.is_negative(): - raise ValueError(f"Token amount must be non-negative ({token_amount})!") - return token_amount - - @classmethod - def parse_amount( - cls, amt_type: Optional[Amount], token: Token - ) -> Optional[TokenBalance]: - """Auxiliary method to parse a numerical value into a TokenBalance. - - Args: - amt_type: Amount to be set, or None. - token: Token belonging to amount. - - Returns: - A TokenBalance, or None. - - """ - - if isinstance(amt_type, (int, float, Decimal)): - return cls(amt_type, token) - - if isinstance(amt_type, TokenBalance): - if amt_type.token != token: - raise ValueError( - f"Tokens do not match: <{amt_type.token}> vs. <{token}>!" - ) - return amt_type - - return None - - def as_decimal(self) -> Decimal: - """Returns balance attribute as Decimal type""" - return self._balance - - @staticmethod - def precision() -> int: - """Return precision currently associated with TokenBalance.""" - return getcontext().prec - - def is_positive(self) -> bool: - """Determine if a TokenBalance is positive.""" - return self._balance > 0 - - def is_negative(self) -> bool: - """Determine if a TokenBalance is negative.""" - return self._balance < 0 - - def is_zero(self) -> bool: - """Determine if a TokenBalance is zero.""" - return self._balance == 0 - - def __eq__(self, other: object) -> bool: - """Equality operator. - - Args: - other: Another TokenBalance, or zero. - """ - if other == 0: - return self.is_zero() - - if isinstance(other, TokenBalance): - - if self.token != other.token: - raise ValueError( - f"Cannot compare different tokens <{self.token}> / <{other.token}>!" - ) - return self._balance == other._balance - - raise ValueError(f"Cannot compare TokenBalance and type <{type(other)}>!") - - def __ne__(self, other: object) -> bool: - """Non-equality operator""" - return not self == other - - def __lt__(self, other: Union[TokenBalance, NumericType]) -> bool: - """Less-than operator. - - Args: - other: Another TokenBalance, or zero. - """ - if isinstance(other, TokenBalance): - - if self.token != other.token: - raise ValueError( - f"Cannot compare different tokens <{self.token}> / <{other.token}>!" - ) - return self._balance < other._balance - - if other == 0: - return self._balance < 0 - - raise ValueError(f"Cannot compare TokenBalance and type <{type(other)}>") - - def __le__(self, other: Union[TokenBalance, NumericType]) -> bool: - """Less-than-or-equal operator. - - Args: - other: Another TokenBalance, or zero. - """ - return self < other or self == other - - def __gt__(self, other: Union[TokenBalance, NumericType]) -> bool: - """Greater-than operator. - - Args: - other: Another TokenBalance, or zero. - """ - return not self <= other - - def __ge__(self, other: Union[TokenBalance, NumericType]) -> bool: - """Greater-than-or-equal operator. - - Args: - other: Another TokenBalance, or zero. - """ - return self > other or self == other - - def __neg__(self) -> TokenBalance: - """Negation operator.""" - return TokenBalance(-self._balance, self.token) - - def __abs__(self) -> TokenBalance: - """Absolute value operator.""" - return TokenBalance(abs(self._balance), self.token) - - def __add__(self, other: Union[TokenBalance, NumericType]) -> TokenBalance: - """Addition operator. - - Args: - other: Another TokenBalance, or zero. - """ - if isinstance(other, TokenBalance): - - if self.token == other.token: - return TokenBalance(self._balance + other._balance, self.token) - - raise ValueError(f"Cannot add <{other.token}> and <{self.token}>!") - - if other == 0: - # This is required to enable the use of the sum() function: - # sum() by design starts with a value of '0' and then iteratively - # adds the items in the list that is passed as argument. See: - # https://stackoverflow.com/questions/1218710/pythons-sum-and-non-integer-values - return self - - raise ValueError(f"Cannot add <{type(other)}> and TokenBalance!") - - def __radd__(self, other: Union[TokenBalance, NumericType]) -> TokenBalance: - """Addition-from-right operator. - - Args: - other: Another TokenBalance, or zero. - """ - return self + other - - def __sub__(self, other: Union[TokenBalance, NumericType]) -> TokenBalance: - """Subtraction operator. - - Args: - other: Another TokenBalance, or zero. - """ - return self + (-other) - - def __rsub__(self, other: Union[TokenBalance, NumericType]) -> TokenBalance: - """Subtraction operator. - - Args: - other: Another TokenBalance, or zero. - """ - return other + (-self) - - def __mul__(self, other: NumericType) -> TokenBalance: - """Multiplication operator. - - Args: - other: A {float|int|Decimal}. - """ - if isinstance(other, (int, float, Decimal)): - return TokenBalance(Decimal(other) * self._balance, self.token) - - raise ValueError(f"Cannot multiply TokenBalance by <{type(other)}>!") - - def __rmul__(self, other: NumericType) -> TokenBalance: - """Multiplication-from-right operator. - - Args: - other: A {float|int|Decimal}. - """ - return self * other - - def __truediv__(self, other: Union[TokenBalance, NumericType]) -> TokenBalance: - """Division operator. - - Args: - other: A {TokenBalance|float|int|Decimal}. - """ - if isinstance(other, (int, float, Decimal)): - if other == 0: - raise ZeroDivisionError - return TokenBalance(self._balance / Decimal(other), self.token) - - if isinstance(other, TokenBalance): - if self.token == other.token: - return TokenBalance(self._balance / other._balance, self.token) - raise ValueError( - f"Can't divide TokenBalances with different " - f"tokens <{self.token}> and <{other.token}>!" - ) - - raise ValueError(f"Cannot divide TokenBalance by <{type(other)}>!") - - def __rtruediv__(self, other: object) -> None: - """Division-from-right operator. - - Args: - other: Something. - """ - raise ValueError(f"<{type(other)}> cannot be divided by TokenBalance!") - - def __str__(self) -> str: - """Represent as string (rounded to 5 decimals).""" - return f"{self.token}: {self.balance}" - - def __repr__(self) -> str: - """Represent as string.""" - return str(self) - - -Amount = Union[TokenBalance, NumericType] diff --git a/src/models/types.py b/src/models/types.py deleted file mode 100644 index 06e458e..0000000 --- a/src/models/types.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Generic Type derived from primitives and imports""" -from decimal import Decimal -from typing import Union - -NumericType = Union[int, float, Decimal] diff --git a/src/models/uniswap.py b/src/models/uniswap.py deleted file mode 100644 index 386f82b..0000000 --- a/src/models/uniswap.py +++ /dev/null @@ -1,322 +0,0 @@ -"""Representation of Uniswap pool.""" -from __future__ import annotations - -import json -import logging -from decimal import Decimal -from typing import Optional, Any, Union - -from src.models.exchange_rate import ExchangeRate as XRate -from src.models.token import Token, TokenBalance -from src.models.types import NumericType -from src.util.enums import AMMKind -from src.util.exec_plan_coords import ExecPlanCoords -from src.util.numbers import decimal_to_str - -FeeType = Union[float, Decimal] -UniswapSerializedType = dict[str, Any] -UniswapsSerializedType = dict[str, UniswapSerializedType] - - -class Uniswap: - """Representation of an Automated Market Maker. - - An Uniswap pool is represented by two token balances. - """ - - def __init__( - self, - pool_id: str, - balance1: TokenBalance, - balance2: TokenBalance, - fee: FeeType, - cost: Optional[TokenBalance] = None, - mandatory: bool = False, - kind: AMMKind = AMMKind.UNISWAP, - weight: float = 1, - ): - """Initialize. - - Args: - pool_id: Uniswap pool pool_id. - balance1: TokenBalance of first token. - balance2: TokenBalance of second token. - fee: Uniswap fee percentage. - - Kwargs: - cost: Cost of using the Uniswap pool. - mandatory: Is pool usage mandatory when price moves, or not. - """ - # Consistency checks. - if balance1.token == balance2.token: - logging.error("Pool tokens cannot be equal!") - raise ValueError - - if not all(tb.is_positive() for tb in [balance1, balance2]): - message = f"Uniswap <{pool_id}>: balance1={balance1} balance2={balance2}" - logging.error(message) - raise ValueError("Both token balances must be positive!") - - # Store given pool pool_id. - self.pool_id = pool_id - self.balance1 = balance1 - self.balance2 = balance2 - self.fee = fee if isinstance(fee, Decimal) else Decimal(fee) - self.cost = cost - self.mandatory = mandatory - self.kind = kind - self.weight = weight - - self._balance_update1: Optional[TokenBalance] = None - self._balance_update2: Optional[TokenBalance] = None - self.exec_plan_coords: Optional[ExecPlanCoords] = None - - @classmethod - def from_dict( - cls, amm_id: str, amm_data: UniswapSerializedType - ) -> Optional[Uniswap]: - """Construct AMM object from data dict. - NOTE: Currently, the code only supports Uniswap-style AMMs, i.e., - constant-product pools with two tokens and equal weights. - Args: - amm_id: AMM pool_id. - amm_data: Dict of uniswap data. - Returns: - A Uniswap object. - """ - for attr in ["kind", "reserves", "fee"]: - if attr not in amm_data: - raise ValueError(f"Missing field '{attr}' in amm <{amm_id}>!") - - kind = AMMKind(amm_data["kind"]) - reserves = amm_data.get("reserves") - weight = 0.5 - - if kind == AMMKind.CONSTANT_PRODUCT: - # Parse UniswapV2/Sushiswap pools. - if not isinstance(reserves, dict): - raise ValueError( - f"AMM <{amm_id}>: 'reserves' must be a dict of Token -> amount!" - ) - if len(reserves) != 2: - message = ( - f"AMM <{amm_id}>: " - f"ConstantProduct AMMs are only supported with 2 tokens!" - ) - logging.warning(message) - return None - balance1, balance2 = [ - TokenBalance(Decimal(b), Token(t)) for t, b in reserves.items() - ] - - elif kind == AMMKind.WEIGHTED_PRODUCT: - # Parse Balancer weighted constant-product pools. - if not ( - isinstance(reserves, dict) - and all( - isinstance(reserve_info, dict) and key in reserve_info - for reserve_info in reserves.values() - for key in ["balance", "weight"] - ) - ): - raise ValueError( - f"AMM <{amm_id}>: 'reserves' must be a dict " - f"of Token -> {'balance': .., 'weight': ..}" - ) - if ( - len(reserves) != 2 - or len(set(b["weight"] for b in reserves.values())) > 1 - ): - logging.warning( - f"AMM <{amm_id}>: WeightedProduct AMMs are only supported " - "with 2 tokens and equal weights!" - ) - return None - - weight = list(reserves.values())[0]["weight"] - balance1, balance2 = [ - TokenBalance(Decimal(b["balance"]), Token(t)) - for t, b in reserves.items() - ] - - else: - logging.warning( - f"AMM <{amm_id}>: type <{kind}> is currently not supported!" - ) - return None - - if balance1 == 0 or balance2 == 0: - return None - - return Uniswap( - pool_id=amm_id, - balance1=balance1, - balance2=balance2, - fee=Decimal(amm_data["fee"]), - cost=TokenBalance.parse(amm_data.get("cost"), allow_none=True), - kind=kind, - weight=weight, - ) - - def as_dict(self) -> dict: - """Return AMM object as dictionary. - - NOTE: Currently, the code only supports Uniswap-style AMMs, i.e., - constant-product pools with two tokens and equal weights. - - """ - token1 = str(self.token1) - token2 = str(self.token2) - balance1 = decimal_to_str(self.balance1.as_decimal()) - balance2 = decimal_to_str(self.balance2.as_decimal()) - - reserves: Union[str, dict] - if self.kind == AMMKind.WEIGHTED_PRODUCT: - reserves = { - token1: {"balance": balance1, "weight": self.weight}, - token2: {"balance": balance2, "weight": self.weight}, - } - else: - reserves = {token1: balance1, token2: balance2} - - cost = None - if self.cost is not None: - cost = { - "token": str(self.cost.token), - "amount": decimal_to_str(self.cost.as_decimal()), - } - - execution = {} - if self.is_executed(): - assert self.balance_update1 is not None and self.balance_update2 is not None - b1_update = self.balance_update1.as_decimal() - b2_update = self.balance_update2.as_decimal() - # One update has to be positive and the other negative. - assert ( - b1_update * b2_update < 0 - ), f"Failed assertion, {b1_update} * {b2_update} < 0" - - # Determine buy- and sell-tokens and -amounts. - if b1_update > 0: - buy_token = self.token1 - sell_token = self.token2 - exec_buy_amount = b1_update - exec_sell_amount = -b2_update - - else: - buy_token = self.token2 - sell_token = self.token1 - exec_buy_amount = b2_update - exec_sell_amount = -b1_update - - if self.exec_plan_coords is None: - logging.warning( - f"AMM <{self.pool_id}>: " - f"has balance updates with invalid execution plan" - ) - exec_plan = None - else: - exec_plan = self.exec_plan_coords.as_dict() - - execution = { - "sell_token": str(sell_token), - "buy_token": str(buy_token), - "exec_sell_amount": decimal_to_str(exec_sell_amount), - "exec_buy_amount": decimal_to_str(exec_buy_amount), - "exec_plan": exec_plan, - } - - return { - "kind": str(self.kind), - "reserves": reserves, - "cost": cost, - "fee": decimal_to_str(self.fee), - "execution": execution, - } - - #################### - # ACCESS METHODS # - #################### - - @property - def token1(self) -> Token: - """Returns token1""" - return self.balance1.token - - @property - def token2(self) -> Token: - """Returns token2""" - return self.balance2.token - - @property - def tokens(self) -> set[Token]: - """Return the pool tokens.""" - return {self.balance1.token, self.balance2.token} - - @property - def balance_update1(self) -> Optional[TokenBalance]: - """Return the traded amount of the first token.""" - return self._balance_update1 - - @property - def balance_update2(self) -> Optional[TokenBalance]: - """Return the traded amount of the second token.""" - return self._balance_update2 - - def other_token(self, token: Token) -> Token: - """Returns the "other" token that is not token.""" - assert token in self.tokens - return (self.tokens - {token}).pop() - - ##################### - # UTILITY METHODS # - ##################### - - def execute( - self, - b1_update: NumericType, - b2_update: NumericType, - ) -> None: - """Execute the uniswap at given amounts. - - Args: - b1_update: Traded amount of token1. - b2_update: Traded amount of token2. - """ - assert isinstance(b1_update, (int, float, Decimal)) - assert isinstance(b2_update, (int, float, Decimal)) - - # Store execution information. - self._balance_update1 = TokenBalance(b1_update, self.token1) - self._balance_update2 = TokenBalance(b2_update, self.token2) - - def is_executed(self) -> bool: - """True if amm is executed otherwise false""" - return self.balance_update1 is not None and self.balance_update2 is not None - - def get_marginal_xrate(self) -> XRate: - """Derive the marginal exchange rate from the pool balances.""" - return XRate(self.balance1, self.balance2) - - def __str__(self) -> str: - """Represent as string.""" - return json.dumps(self.as_dict(), indent=2) - - def __repr__(self) -> str: - """Represent as short string.""" - return f"u{self.pool_id}" - - def __hash__(self) -> int: - return hash(self.pool_id) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, Uniswap): - return NotImplemented - if self.pool_id != other.pool_id: - return False - return True - - def __lt__(self, other: object) -> bool: - if not isinstance(other, Uniswap): - return NotImplemented - return self.pool_id < other.pool_id diff --git a/src/util/constants.py b/src/util/constants.py deleted file mode 100644 index a2c27d7..0000000 --- a/src/util/constants.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Global constants.""" - -from decimal import Decimal - - -class Constants: - """Configuration parameters for the solver.""" - - # Precision of Decimal in strings (to be used as x.quantize(DECIMAL_STR_PREC)). - DECIMAL_STR_PREC = Decimal("1e-10") - - # Should an exception be raised when the solution violates the - # max sell amount constraint. - RAISE_ON_MAX_SELL_AMOUNT_VIOLATION = False - - # Should an exception be raised when the solution violates the - # limit exchange rate constraint. - RAISE_ON_LIMIT_XRATE_VIOLATION = False diff --git a/src/util/enums.py b/src/util/enums.py deleted file mode 100644 index 6bb6f8a..0000000 --- a/src/util/enums.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Location of all Enum types""" - -from enum import Enum - - -class AMMKind(Enum): - """Enum for different AMM kinds.""" - - UNISWAP = "Uniswap" - CONSTANT_PRODUCT = "ConstantProduct" - WEIGHTED_PRODUCT = "WeightedProduct" - STABLE = "Stable" - CONCENTRATED = "Concentrated" - - def __str__(self) -> str: - """Represent as string.""" - return self.value - - def __repr__(self) -> str: - """Represent as string.""" - return str(self) - - -class Chain(Enum): - """Enum for the blockchain of the batch auction.""" - - MAINNET = "MAINNET" - XDAI = "XDAI" - UNKNOWN = "UNKNOWN" - - def __str__(self) -> str: - """Represent as string.""" - return self.name - - def __repr__(self) -> str: - """Represent as string.""" - return str(self) diff --git a/src/util/exec_plan_coords.py b/src/util/exec_plan_coords.py deleted file mode 100644 index befc26b..0000000 --- a/src/util/exec_plan_coords.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Execution Plan Coordinates""" - - -class ExecPlanCoords: - """The position coordinates of the uniswap in the execution plan. - - The position is defined by a pair of integers: - * Id of the sequence. - * Position within that sequence. - """ - - def __init__(self, sequence: int, position: int): - self.sequence = sequence - self.position = position - - def as_dict(self) -> dict[str, str]: - """returns string dict of class""" - return { - "sequence": str(self.sequence), - "position": str(self.position), - } diff --git a/src/util/numbers.py b/src/util/numbers.py deleted file mode 100644 index 7967359..0000000 --- a/src/util/numbers.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Utility methods for number handling""" -from decimal import Decimal - - -def decimal_to_str(number: Decimal) -> str: - """Converts Decimal to string""" - try: - return f"{round(float(number), 12):.12f}".rstrip("0").rstrip(".") - except ValueError as err: - raise ValueError(f"Could not convert <{number}> into a string!") from err diff --git a/src/util/schema.py b/src/util/schema.py deleted file mode 100644 index f9f3917..0000000 --- a/src/util/schema.py +++ /dev/null @@ -1,399 +0,0 @@ -""" -This file defines problem, solution, and solver parameters schemas, -using pydantic that is then used to validate IO and autogenerate -documentation. -""" - -from typing import Dict, List, Optional, Union - -from enum import Enum -from pydantic import BaseModel, Field - -# Example instance to use in the autogenerated API documentation. - -example_instance = { - "metadata": { - "environment": "xDAI", - "auction_id": 1, - "gas_price": 4850000000.0, - "native_token": "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", - }, - "tokens": { - "0x6b175474e89094c44da98b954eedeac495271d0f": { - "decimals": 18, - "alias": "DAI", - "external_price": 0.00021508661247926934, - "normalize_priority": 0, - "internal_buffer": "8213967696976545926330", - }, - "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": { - "decimals": 6, - "alias": "USDC", - "external_price": 214890212.34875953, - "normalize_priority": 0, - "internal_buffer": "2217249148", - }, - "0xdac17f958d2ee523a2206206994597c13d831ec7": { - "decimals": 6, - "alias": "USDT", - "external_price": 214523029.31427807, - "normalize_priority": 0, - "internal_buffer": "4227015605", - }, - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": { - "decimals": 18, - "alias": "WETH", - "external_price": 1.0, - "normalize_priority": 1, - "internal_buffer": "895880027660372311", - }, - }, - "orders": { - "0": { - "sell_token": "0x6b175474e89094c44da98b954eedeac495271d0f", - "buy_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "sell_amount": "4693994755140375611596", - "buy_amount": "1000000000000000000", - "allow_partial_fill": False, - "is_sell_order": False, - "fee": { - "amount": "103079335446226157568", - "token": "0x6b175474e89094c44da98b954eedeac495271d0f", - }, - "cost": { - "amount": "6657722265694875", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - }, - "is_liquidity_order": False, - }, - "1": { - "sell_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "buy_token": "0x6b175474e89094c44da98b954eedeac495271d0f", - "sell_amount": "1000000000000000000", - "buy_amount": "4692581049969374626065", - "allow_partial_fill": False, - "is_sell_order": True, - "fee": { - "amount": "23212472598551576", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - }, - "cost": { - "amount": "6657722265694875", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - }, - "is_liquidity_order": True, - }, - }, - "amms": { - "01": { - "kind": "ConstantProduct", - "reserves": { - "0x6b175474e89094c44da98b954eedeac495271d0f": "44897630044876228891318837", - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "9626911517235794223708", - }, - "fee": "0.003", - "cost": { - "amount": "9507044675748200", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - }, - "mandatory": False, - }, - "02": { - "kind": "ConstantProduct", - "reserves": { - "0x6b175474e89094c44da98b954eedeac495271d0f": "84903768350604287941150958", - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "18233677073990818080605", - }, - "fee": "0.003", - "cost": { - "amount": "9507044675748200", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - }, - "mandatory": False, - }, - "03": { - "kind": "WeightedProduct", - "reserves": { - "0x6b175474e89094c44da98b954eedeac495271d0f": { - "balance": "1191959749018354276837", - "weight": "0.4", - }, - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": { - "balance": "392171457910841840", - "weight": "0.6", - }, - }, - "fee": "0.0025", - "cost": { - "amount": "12047450379000000", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - }, - "mandatory": False, - }, - "04": { - "kind": "WeightedProduct", - "reserves": { - "0x6810e776880c02933d47db1b9fc05908e5386b96": { - "balance": "21330539255670269346", - "weight": "0.25", - }, - "0x6b175474e89094c44da98b954eedeac495271d0f": { - "balance": "10928595376682871418747", - "weight": "0.25", - }, - "0xba100000625a3754423978a60c9317c58a424e3d": { - "balance": "444658133648670940819", - "weight": "0.25", - }, - "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": { - "balance": "2237408990689298635", - "weight": "0.25", - }, - }, - "fee": "0.01", - "cost": { - "amount": "12047450379000000", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - }, - "mandatory": False, - }, - }, -} - - -# The following classes model the contents of a PROBLEM instance. -# They are used for input validation and documentation. - - -class TokenId(str): - """Token unique identifier.""" - - -class OrderId(str): - """Order unique identifier.""" - - -class AmmId(str): - """AMM unique identifier.""" - - -class BigInt(str): - """Big integer (as a string).""" - - -class Decimal(str): - """Decimal number (as a string).""" - - -class TokenInfoModel(BaseModel): - """Token-specific data.""" - - decimals: Optional[int] = Field(None, description="Number of decimals.") - alias: Optional[str] = Field(None, description="Human-readable name (e.g. DAI).") - normalize_priority: Optional[int] = Field( - 0, - description="Priority for solution price vector normalization purposes " - "(larger=higher preference).", - ) - external_price: Optional[Decimal] = Field(None, description="External token price.") - internal_buffer: Optional[BigInt] = Field( - None, description="Internal token buffer." - ) - - -class TokenAmountModel(BaseModel): - """Order/AMM cost and order fee.""" - - amount: BigInt = Field(..., description="Amount.") - token: TokenId = Field(..., description="Token.") - - -class OrderModel(BaseModel): - """Order data.""" - - sell_token: TokenId = Field(..., description="Token to be sold.") - buy_token: TokenId = Field(..., description="Token to be bought.") - sell_amount: BigInt = Field( - ..., - description="If is_sell_order=true indicates the maximum amount to sell, " - "otherwise the maximum amount to sell in order to buy buy_amount.", - ) - buy_amount: BigInt = Field( - ..., - description="If is_sell_order=false indicates the maximum amount to buy, " - "otherwise the minimum amount to buy in order to sell sell_amount.", - ) - allow_partial_fill: bool = Field( - ..., - description="If the order can sell/buy less than its maximum sell/buy amount.", - ) - is_sell_order: bool = Field( - ..., - description="If it is a sell or buy order, changing the semantics of " - "sell_amount/buy_amount accordingly.", - ) - is_liquidity_order: Optional[bool] = Field( - False, - description="Liquidity orders (from market makers) can not receive surplus.", - ) - has_atomic_execution: Optional[bool] = Field( - False, description="Indicates, if the order needs to be executed atomically." - ) - fee: Optional[TokenAmountModel] = Field( - None, - description="Fee contribution when order is matched " - "(pro-rata for partial matching).", - ) - cost: Optional[TokenAmountModel] = Field( - None, description="Cost of matching the order." - ) - - class Config: - """Includes example in generated openapi file""" - - schema_extra = { - "example": { - "sell_token": "0x6b175474e89094c44da98b954eedeac495271d0f", - "buy_token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - "sell_amount": "4693994755140375611596", - "buy_amount": "1000000000000000000", - "allow_partial_fill": False, - "is_sell_order": False, - "fee": { - "amount": "103079335446226157568", - "token": "0x6b175474e89094c44da98b954eedeac495271d0f", - }, - "cost": { - "amount": "6657722265694875", - "token": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", - }, - "is_liquidity_order": False, - } - } - - -class AmmKindEnum(str, Enum): - """AMM kind.""" - - CONSTANT_PRODUCT = "ConstantProduct" - WEIGHTED_PRODUCT = "WeightedProduct" - STABLE = "Stable" - CONCENTRATED = "Concentrated" - - -class ConstantProductReservesModel(BigInt): - """Tokens and balances of constant-product AMMs.""" - - -class WeightedProductReservesModel(BaseModel): - """Tokens and balances+weights of weighted-product AMMs.""" - - balance: BigInt = Field(..., description="Token balance in AMM.") - weight: BigInt = Field(..., description="Weight of the token.") - - -class AmmModel(BaseModel): - """AMM data.""" - - kind: AmmKindEnum = Field(..., description="AMM type.") - reserves: Optional[ - Dict[TokenId, Union[ConstantProductReservesModel, WeightedProductReservesModel]] - ] = Field(None, description="AMM tokens and balances.") - fee: Optional[Decimal] = Field( - None, description="AMM trading fee (e.g. 0.003 for 0.3% fee)." - ) - cost: Optional[TokenAmountModel] = Field( - None, description="Cost of using the pool." - ) - - -class MetadataModel(BaseModel): - """Batch auction metadata.""" - - environment: Optional[str] = Field( - None, description="Runtime/blockchain environment." - ) - auction_id: Optional[str] = Field(..., description="Max Number of executed orders") - gas_price: Optional[float] = Field(..., description="Current Gas price") - native_token: Optional[TokenId] = Field(..., description="Wrapped Native Token") - - -class BatchAuctionModel(BaseModel): - """Batch auction instance data.""" - - tokens: Dict[TokenId, TokenInfoModel] = Field(..., description="Tokens.") - orders: Dict[OrderId, OrderModel] = Field(..., description="Orders.") - metadata: MetadataModel = Field({}, description="Metadata.") - amms: Optional[Dict[AmmId, AmmModel]] = Field({}, description="AMMs") - - class Config: - """Includes example in generated openapi file""" - - schema_extra = {"example": example_instance} - - -# The following classes model the contents of a SOLUTION instance. -# They are used for input validation and documentation. - - -class ExecutedOrderModel(OrderModel): - """Executed order data (solution).""" - - exec_buy_amount: BigInt = Field(..., description="Executed buy amount.") - exec_sell_amount: BigInt = Field(..., description="Executed sell amount.") - - -class ExecPlanCoordsModel(BaseModel): - """Execution plan coordinates.""" - - sequence: int = Field(..., description="Sequence index.") - position: int = Field(..., description="Position within the sequence.") - internal: Optional[bool] = Field(False, description="Using internal liquidity") - - -class AmmExecutionModel(BaseModel): - """AMM settlement information.""" - - sell_token: TokenId = Field(..., description="Token sold by the AMM.") - buy_token: TokenId = Field(..., description="Token bought by the AMM.") - exec_sell_amount: BigInt = Field(..., description="Executed sell amount.") - exec_buy_amount: BigInt = Field(..., description="Executed buy amount.") - exec_plan: Optional[ExecPlanCoordsModel] = Field( - None, description="Execution plan coordinates." - ) - - -class ExecutedAmmModel(AmmModel): - """List of AMM executions.""" - - execution: Optional[List[AmmExecutionModel]] = Field( - None, description="AMM settlement data." - ) - - -class InteractionData(BaseModel): - """Interaction data.""" - - target: TokenId = Field( - ..., description="Target contract address to interact with." - ) - value: BigInt = Field( - ..., description="Value of native token, e.g. amount eth in eth transfer" - ) - call_data: bytes = Field(..., description="Interaction encoding.") - - -class SettledBatchAuctionModel(BaseModel): - """Settled batch auction data (solution).""" - - orders: Dict[OrderId, ExecutedOrderModel] = Field( - ..., description="Executed orders." - ) - prices: Dict[TokenId, BigInt] = Field( - ..., description="Settled price for each token." - ) - amms: Dict[AmmId, ExecutedAmmModel] = Field(..., description="Executed AMMs.") - - interaction_data: List[InteractionData] = Field( - description="List of interaction data.", default=[] - ) diff --git a/src/util/__init__.py b/tests/__init__.py similarity index 100% rename from src/util/__init__.py rename to tests/__init__.py diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py new file mode 100644 index 0000000..a812c6b --- /dev/null +++ b/tests/test_endpoints.py @@ -0,0 +1,158 @@ +"""Tests for the API endpoints.""" + +from fastapi.testclient import TestClient +from src._server import app + +client = TestClient(app) + + +def test_notify() -> None: + """Check notify endpoint.""" + response = client.post("/notify", json={"note": "everything is fine"}) + assert response.status_code == 200 + assert response.json() is True + + +def test_solve() -> None: + """Check solve endpoints.""" + auction = { + "id": "string", + "tokens": { + "additionalProp1": { + "decimals": 0, + "symbol": "string", + "referencePrice": "1234567890", + "availableBalance": "1234567890", + "trusted": True, + }, + "additionalProp2": { + "decimals": 0, + "symbol": "string", + "referencePrice": "1234567890", + "availableBalance": "1234567890", + "trusted": True, + }, + "additionalProp3": { + "decimals": 0, + "symbol": "string", + "referencePrice": "1234567890", + "availableBalance": "1234567890", + "trusted": True, + }, + }, + "orders": [ + { + "uid": "0x30cff40d9f60caa68a37f0ee73253ad6ad72b45580c945fe3ab67596476937197854163b1b0d24e77dca702b97b5cc33e0f83dcb626122a6", # pylint: disable=line-too-long + "sellToken": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", + "buyToken": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", + "sellAmount": "1234567890", + "buyAmount": "1234567890", + "feeAmount": "1234567890", + "kind": "sell", + "partiallyFillable": True, + "class": "market", + } + ], + "liquidity": [ + { + "kind": "constantproduct", + "tokens": { + "additionalProp1": {"balance": "1234567890"}, + "additionalProp2": {"balance": "1234567890"}, + "additionalProp3": {"balance": "1234567890"}, + }, + "fee": "13.37", + "id": "string", + "address": "0x0000000000000000000000000000000000000000", + "gasEstimate": "1234567890", + }, + { + "kind": "weightedproduct", + "tokens": { + "additionalProp1": { + "balance": "1234567890", + "scalingFactor": "1234567890", + "weight": "13.37", + }, + "additionalProp2": { + "balance": "1234567890", + "scalingFactor": "1234567890", + "weight": "13.37", + }, + "additionalProp3": { + "balance": "1234567890", + "scalingFactor": "1234567890", + "weight": "13.37", + }, + }, + "fee": "13.37", + "id": "string", + "address": "0x0000000000000000000000000000000000000000", + "gasEstimate": "1234567890", + }, + { + "kind": "stable", + "tokens": { + "additionalProp1": { + "balance": "1234567890", + "scalingFactor": "1234567890", + }, + "additionalProp2": { + "balance": "1234567890", + "scalingFactor": "1234567890", + }, + "additionalProp3": { + "balance": "1234567890", + "scalingFactor": "1234567890", + }, + }, + "amplificationParameter": "13.37", + "fee": "13.37", + "id": "string", + "address": "0x0000000000000000000000000000000000000000", + "gasEstimate": "1234567890", + }, + { + "kind": "concentratedliquidity", + "tokens": ["0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB"], + "sqrtPrice": "1234567890", + "liquidity": "1234567890", + "tick": 0, + "liquidityNet": { + "additionalProp1": "1234567890", + "additionalProp2": "1234567890", + "additionalProp3": "1234567890", + }, + "fee": "13.37", + "id": "string", + "address": "0x0000000000000000000000000000000000000000", + "gasEstimate": "1234567890", + }, + { + "kind": "limitorder", + "hash": "0x1e66721bb1bd77d2641c77ea1d61e8abb92bf69c64fcc90c2c6ad518d1b50db1", + "makerToken": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", + "takerToken": "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", + "makerAmount": "1234567890", + "takerAmount": "1234567890", + "takerTokenFeeAmount": "1234567890", + "id": "string", + "address": "0x0000000000000000000000000000000000000000", + "gasEstimate": "1234567890", + }, + ], + "effectiveGasPrice": "1234567890", + "deadline": "1970-01-01T00:00:00.000Z", + } + trivial_solution = { + "id": "123", + "trades": [], + "prices": {}, + "interactions": [], + "solver": "solvertemplate", + "score": "0", + "weth": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", + } + + response = client.post("/solve", json=auction) + assert response.json() == trivial_solution diff --git a/tests/unit/test_order.py b/tests/unit/test_order.py deleted file mode 100644 index 70b379c..0000000 --- a/tests/unit/test_order.py +++ /dev/null @@ -1,98 +0,0 @@ -import unittest - -from src.models.order import Order, OrderMatchType -from src.models.token import Token - - -class MyTestCase(unittest.TestCase): - def setUp(self) -> None: - self.token_a = "0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1" - self.token_b = "0x177127622c4a00f3d409b75571e12cb3c8973d3c" - self.token_c = "0x1111111111111111111111111111111111111111" - - def overlapping_orders(self): - order1 = Order.from_dict( - "1", - { - "sell_token": self.token_a, - "buy_token": self.token_b, - "sell_amount": "12", - "buy_amount": "100", - "allow_partial_fill": False, - "is_sell_order": True, - "fee": { - "amount": "115995469750", - "token": "0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1", - }, - "cost": { - "amount": "321627750000000", - "token": "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", - }, - "is_liquidity_order": False, - "mandatory": False, - "has_atomic_execution": False, - }, - ) - order2 = Order.from_dict( - "2", - { - "sell_token": self.token_b, - "buy_token": self.token_a, - "sell_amount": "100", - "buy_amount": "10", - "allow_partial_fill": False, - "is_sell_order": True, - "fee": { - "amount": "115995469750", - "token": "0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1", - }, - "cost": { - "amount": "321627750000000", - "token": "0xe91d153e0b41518a2ce8dd3d7944fa863463a97d", - }, - "is_liquidity_order": False, - "mandatory": False, - "has_atomic_execution": False, - }, - ) - - return order1, order2 - - def test_overlaps(self): - - order1, order2 = self.overlapping_orders() - self.assertTrue(order1.overlaps(order2)) - - # Change the buy amount to make it so the orders don't overlap. - old_buy_amount = order1.buy_amount - - order1.buy_amount *= 10 - self.assertFalse(order1.overlaps(order2)) - - # Set the buy amount back and change the token. - order1.buy_amount = old_buy_amount - self.assertTrue(order1.overlaps(order2)) - token_c = "0x1111111111111111111111111111111111111111" - order1.buy_token = Token(token_c) - self.assertFalse(order1.overlaps(order2)) - - def test_match_type(self): - order1, order2 = self.overlapping_orders() - - self.assertEqual(order1.match_type(order2), OrderMatchType.BOTH_FILLED) - - # Make order1 half the size - order1.buy_amount /= 2 - order1.sell_amount /= 2 - - self.assertEqual(order1.match_type(order2), OrderMatchType.LHS_FILLED) - # Reverse the orders to get RHS Filled - self.assertEqual(order2.match_type(order1), OrderMatchType.RHS_FILLED) - - order1.buy_token = Token(self.token_c) - - self.assertIsNone(order1.match_type(order2)) - - -if __name__ == "__main__": - unittest.main()