Skip to content

Bare bones solver #30

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -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
6 changes: 1 addition & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
78 changes: 13 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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
13 changes: 1 addition & 12 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -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
python_version = 3.11
51 changes: 42 additions & 9 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
114 changes: 25 additions & 89 deletions src/_server.py
Original file line number Diff line number Diff line change
@@ -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,
)
Loading