Skip to content
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
77 changes: 67 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: test
name: Test

on:
push:
Expand All @@ -10,30 +7,90 @@ on:
branches: [ master ]

jobs:
build:
test:
name: test (v${{ matrix.uniswap-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
uniswap-version: [1, 2, 3]

steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Set up Node
uses: actions/setup-node@v2-beta
with:
node-version: '12'
- name: Install dependencies

# Set up poetry cache, from https://github.com/python-poetry/poetry/blob/45a9b8f20384591d0a33ae876bcf23656f928ec0/.github/workflows/main.yml
- name: Get full python version
id: full-python-version
run: |
echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info[:3]))")
- name: Set up poetry
run: |
python -m pip install --upgrade pip poetry
poetry config virtualenvs.in-project true
- name: Set up cache
uses: actions/cache@v2
id: cache
with:
path: .venv
key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }}
- name: Ensure cache is healthy
if: steps.cache.outputs.cache-hit == 'true'
run: timeout 10s poetry run pip --version || rm -rf .venv

- name: Install dependencies
run: |
poetry install
npm install -g ganache-cli
- name: Typecheck
run: |
make typecheck
- name: Test
env:
PROVIDER: ${{ secrets.MAINNET_PROVIDER }}
MAINNET_PROVIDER: ${{ secrets.MAINNET_PROVIDER }}
UNISWAP_VERSION: ${{ matrix.uniswap-version }}
run: |
make test

typecheck:
name: typecheck
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.8

# Set up poetry cache, from https://github.com/python-poetry/poetry/blob/45a9b8f20384591d0a33ae876bcf23656f928ec0/.github/workflows/main.yml
- name: Get full python version
id: full-python-version
run: |
echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info[:3]))")
- name: Set up poetry
run: |
python -m pip install --upgrade pip poetry
poetry config virtualenvs.in-project true
- name: Set up cache
uses: actions/cache@v2
id: cache
with:
path: .venv
key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }}
- name: Ensure cache is healthy
if: steps.cache.outputs.cache-hit == 'true'
run: timeout 10s poetry run pip --version || rm -rf .venv

- name: Install dependencies
run: |
python -m pip install --upgrade pip poetry
poetry install
- name: Typecheck
run: |
make typecheck
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ typecheck:
lint:
poetry run flake8

format:
black uniswap

format-abis:
npx prettier --write --parser=json uniswap/assets/*/*.abi

precommit:
make typecheck
make lint
Expand Down
226 changes: 111 additions & 115 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ web3 = "^5.12.0"
click = "^7.1.2"

[tool.poetry.dev-dependencies]
pytest = "^5.4.3"
mypy = "*"
black = "^19.10b0"
pytest = "^5.4.3"
pytest-cov = "^2.10.0"
pytest-dotenv = "^0.5.2"
python-dotenv = "^0.14.0"
flake8 = "^3.8.3"
mypy = "^0.782"
Sphinx = "^3.5.4"
sphinx-book-theme = "^0.1.0"
sphinx-click = "^2.7.1"
Expand Down
23 changes: 17 additions & 6 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
import json
import sys

from click.testing import CliRunner

from uniswap.cli import main


def print_result(result):
print(result)
print(result.stdout.strip())
print(result.stderr.strip(), file=sys.stderr)


def test_get_price():
runner = CliRunner()
runner = CliRunner(mix_stderr=False)
result = runner.invoke(main, ["price", "weth", "dai"])
print_result(result)
assert result.exit_code == 0

# Will break when ETH breaks 10k
assert 1000 < float(result.output) < 10_000
assert 1000 < float(result.stdout) < 10_000

result = runner.invoke(main, ["price", "wbtc", "dai"])
assert result.exit_code == 0

# Will break when BTC breaks 100k
assert 10_000 < float(result.output) < 100_000
assert 10_000 < float(result.stdout) < 100_000


def test_get_token():
runner = CliRunner()
runner = CliRunner(mix_stderr=False)
result = runner.invoke(main, ["token", "weth"])
print_result(result)
assert result.exit_code == 0
out = json.loads(result.output.replace("'", '"'))

out = json.loads(result.stdout.replace("'", '"'))
assert out["symbol"] == "WETH"
assert out["decimals"] == 18


def test_get_tokendb():
runner = CliRunner()
runner = CliRunner(mix_stderr=False)
result = runner.invoke(main, ["tokendb", "--metadata"])
print_result(result)
assert result.exit_code == 0
92 changes: 63 additions & 29 deletions tests/test_uniswap.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import subprocess
import shutil
import logging
from typing import Generator
from contextlib import contextmanager
from dataclasses import dataclass
Expand All @@ -12,20 +13,44 @@
from uniswap import Uniswap, InvalidToken, InsufficientBalance


logger = logging.getLogger(__name__)

ENV_UNISWAP_VERSION = os.getenv("UNISWAP_VERSION", None)
if ENV_UNISWAP_VERSION:
UNISWAP_VERSIONS = [int(ENV_UNISWAP_VERSION)]
else:
UNISWAP_VERSIONS = [1, 2, 3]


@dataclass
class GanacheInstance:
provider: str
eth_address: str
eth_privkey: str


@pytest.fixture(scope="module", params=[1, 2])
@pytest.fixture(scope="module", params=UNISWAP_VERSIONS)
def client(request, web3: Web3, ganache: GanacheInstance):
uniswap = Uniswap(
return Uniswap(
ganache.eth_address, ganache.eth_privkey, web3=web3, version=request.param
)
uniswap._buy_test_assets()
return uniswap


@pytest.fixture(scope="module")
def test_assets(client: Uniswap):
"""
Buy some DAI and USDC to test with.
"""
tokens = client._get_token_addresses()

for token_name, amount in [("DAI", 100 * 10 ** 18), ("USDC", 100 * 10 ** 6)]:
token_addr = tokens[token_name]
price = client.get_eth_token_output_price(token_addr, amount)
logger.info(f"Cost of {amount} {token_name}: {price}")
logger.info("Buying...")

tx = client.make_trade_output(tokens["ETH"], token_addr, amount)
client.w3.eth.waitForTransactionReceipt(tx)


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -78,6 +103,7 @@ class TestUniswap(object):
weth = Web3.toChecksumAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
bat = Web3.toChecksumAddress("0x0D8775F648430679A709E98d2b0Cb6250d2887EF")
dai = Web3.toChecksumAddress("0x6b175474e89094c44da98b954eedeac495271d0f")
usdc = Web3.toChecksumAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")

# For Rinkeby
# eth = "0x0000000000000000000000000000000000000000"
Expand All @@ -86,10 +112,14 @@ class TestUniswap(object):

# ------ Exchange ------------------------------------------------------------------
def test_get_fee_maker(self, client: Uniswap):
if client.version not in [1, 2]:
pytest.skip("Tested method not supported in this Uniswap version")
r = client.get_fee_maker()
assert r == 0

def test_get_fee_taker(self, client: Uniswap):
if client.version not in [1, 2]:
pytest.skip("Tested method not supported in this Uniswap version")
r = client.get_fee_taker()
assert r == 0.003

Expand Down Expand Up @@ -121,19 +151,21 @@ def test_get_token_eth_input_price(self, client, token, qty):
assert r

@pytest.mark.parametrize(
"token0, token1, qty",
"token0, token1, qty, kwargs",
[
(bat, dai, ONE_ETH),
(dai, bat, ONE_ETH),
(bat, dai, 2 * ONE_ETH),
(weth, dai, ONE_ETH),
(dai, weth, ONE_ETH),
# BAT/DAI has no liquidity in V3
# (bat, dai, ONE_ETH),
# (dai, bat, ONE_ETH),
# (bat, dai, 2 * ONE_ETH),
(dai, usdc, ONE_ETH, {"fee": 500}),
(weth, dai, ONE_ETH, {}),
(dai, weth, ONE_ETH, {}),
],
)
def test_get_token_token_input_price(self, client, token0, token1, qty):
if not client.version == 2:
pytest.skip("Tested method only supported on Uniswap v2")
r = client.get_token_token_input_price(token0, token1, qty)
def test_get_token_token_input_price(self, client, token0, token1, qty, kwargs):
if client.version not in [2, 3]:
pytest.skip("Tested method not supported in this Uniswap version")
r = client.get_token_token_input_price(token0, token1, qty, **kwargs)
assert r

@pytest.mark.parametrize(
Expand Down Expand Up @@ -163,19 +195,19 @@ def test_get_token_eth_output_price(self, client, token, qty):
assert r

@pytest.mark.parametrize(
"token0, token1, qty",
"token0, token1, qty, kwargs",
[
(bat, dai, ONE_ETH),
(dai, bat, ONE_ETH),
(bat, dai, 2 * ONE_ETH),
(weth, dai, ONE_ETH),
(dai, weth, ONE_ETH),
# (bat, dai, ONE_ETH),
(dai, usdc, ONE_ETH, {"fee": 500}),
# (bat, dai, 2 * ONE_ETH),
(weth, dai, ONE_ETH, {}),
(dai, weth, ONE_ETH, {}),
],
)
def test_get_token_token_output_price(self, client, token0, token1, qty):
if not client.version == 2:
pytest.skip("Tested method only supported on Uniswap v2")
r = client.get_token_token_output_price(token0, token1, qty)
def test_get_token_token_output_price(self, client, token0, token1, qty, kwargs):
if client.version not in [2, 3]:
pytest.skip("Tested method not supported in this Uniswap version")
r = client.get_token_token_output_price(token0, token1, qty, **kwargs)
assert r

# ------ ERC20 Pool ----------------------------------------------------------------
Expand Down Expand Up @@ -241,11 +273,11 @@ def test_remove_liquidity(
"input_token, output_token, qty, recipient, expectation",
[
# ETH -> Token
(eth, bat, 1_000_000_000 * ONE_WEI, None, does_not_raise),
(eth, dai, 1_000_000_000 * ONE_WEI, None, does_not_raise),
# Token -> Token
(bat, dai, 1_000_000_000 * ONE_WEI, None, does_not_raise),
(dai, usdc, 1_000_000_000 * ONE_WEI, None, does_not_raise),
# Token -> ETH
(bat, eth, 1_000_000 * ONE_WEI, None, does_not_raise),
(usdc, eth, 1_000_000 * ONE_WEI, None, does_not_raise),
# (eth, bat, 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise),
# (bat, eth, 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise),
# (dai, bat, 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise),
Expand All @@ -256,6 +288,7 @@ def test_make_trade(
self,
client: Uniswap,
web3: Web3,
test_assets,
input_token,
output_token,
qty: int,
Expand All @@ -278,9 +311,9 @@ def test_make_trade(
"input_token, output_token, qty, recipient, expectation",
[
# ETH -> Token
(eth, bat, 1_000_000_000 * ONE_WEI, None, does_not_raise),
(eth, dai, 1_000_000 * ONE_WEI, None, does_not_raise),
# Token -> Token
(bat, dai, 1_000_000_000 * ONE_WEI, None, does_not_raise),
(dai, usdc, 1_000_000 * ONE_WEI, None, does_not_raise),
# Token -> ETH
(dai, eth, 1_000_000 * ONE_WEI, None, does_not_raise),
# FIXME: These should probably be uncommented eventually
Expand All @@ -301,6 +334,7 @@ def test_make_trade_output(
self,
client: Uniswap,
web3: Web3,
test_assets,
input_token,
output_token,
qty: int,
Expand Down
2 changes: 1 addition & 1 deletion uniswap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
from .uniswap import Uniswap, InvalidToken, InsufficientBalance
from .uniswap import Uniswap, InvalidToken, InsufficientBalance, _str_to_addr
from .cli import main
2 changes: 1 addition & 1 deletion uniswap/assets/uniswap-v1/exchange.abi
Original file line number Diff line number Diff line change
Expand Up @@ -1002,4 +1002,4 @@
"type": "function",
"gas": 1713
}
]
]
Loading