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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ Contributors also earn this beautiful [GitPOAP](https://gitpoap.notion.site/What

## Changelog

_0.7.1_

* Updated: Default fee is not applied when using Uniswap V3. Default fee for Uniswap V1 and V2 is still 0.3%.
* Updated: `InvalidFeeTier` exception is raised when a tier is not supported by the protocol version. See all supported tiers in [`uniswap.fee.FeeTier`](./uniswap/fee.py#L12)

_0.7.0_

* incomplete changelog

_0.5.4_

* added use of gas estimation instead of a fixed gas limit (to support Arbitrum)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "uniswap-python"
version = "0.7.0"
version = "0.7.1"
description = "An unofficial Python wrapper for the decentralized exchange Uniswap"
repository = "https://github.com/shanefontaine/uniswap-python"
readme = "README.md"
Expand Down
119 changes: 74 additions & 45 deletions tests/test_uniswap.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
from time import sleep

from web3 import Web3
from web3.types import Wei

from uniswap import Uniswap
from uniswap.constants import ETH_ADDRESS
from uniswap.exceptions import InsufficientBalance
from uniswap.fee import FeeTier
from uniswap.exceptions import InsufficientBalance, InvalidFeeTier
from uniswap.tokens import get_tokens
from uniswap.util import (
_str_to_addr,
Expand Down Expand Up @@ -75,11 +77,11 @@ def test_assets(client: Uniswap):
("USDC", 10_000 * ONE_USDC),
]:
token_addr = tokens[token_name]
price = client.get_price_output(_str_to_addr(ETH_ADDRESS), token_addr, amount)
price = client.get_price_output(_str_to_addr(ETH_ADDRESS), token_addr, amount, fee=FeeTier.TIER_3000)
logger.info(f"Cost of {amount} {token_name}: {price}")
logger.info("Buying...")

txid = client.make_trade_output(tokens["ETH"], token_addr, amount)
txid = client.make_trade_output(tokens["ETH"], token_addr, amount, fee=FeeTier.TIER_3000)
tx = client.w3.eth.wait_for_transaction_receipt(txid, timeout=RECEIPT_TIMEOUT)
assert tx["status"] == 1, f"Transaction failed: {tx}"

Expand Down Expand Up @@ -159,47 +161,47 @@ def test_get_fee_taker(self, client: Uniswap):

# ------ Market --------------------------------------------------------------------
@pytest.mark.parametrize(
"token0, token1, qty, kwargs",
"token0, token1, qty",
[
("ETH", "UNI", ONE_ETH, {}),
("UNI", "ETH", ONE_ETH, {}),
("ETH", "DAI", ONE_ETH, {}),
("DAI", "ETH", ONE_ETH, {}),
("ETH", "UNI", 2 * ONE_ETH, {}),
("UNI", "ETH", 2 * ONE_ETH, {}),
("WETH", "DAI", ONE_ETH, {}),
("DAI", "WETH", ONE_ETH, {}),
("DAI", "USDC", ONE_ETH, {"fee": 500}),
("ETH", "UNI", ONE_ETH),
("UNI", "ETH", ONE_ETH),
("ETH", "DAI", ONE_ETH),
("DAI", "ETH", ONE_ETH),
("ETH", "UNI", 2 * ONE_ETH),
("UNI", "ETH", 2 * ONE_ETH),
("WETH", "DAI", ONE_ETH),
("DAI", "WETH", ONE_ETH),
("DAI", "USDC", ONE_ETH),
],
)
def test_get_price_input(self, client, tokens, token0, token1, qty, kwargs):
def test_get_price_input(self, client: Uniswap, tokens, token0, token1, qty):
token0, token1 = tokens[token0], tokens[token1]
if client.version == 1 and ETH_ADDRESS not in [token0, token1]:
pytest.skip("Not supported in this version of Uniswap")
r = client.get_price_input(token0, token1, qty, **kwargs)
r = client.get_price_input(token0, token1, qty, fee=FeeTier.TIER_3000)
assert r

@pytest.mark.parametrize(
"token0, token1, qty, kwargs",
"token0, token1, qty",
[
("ETH", "UNI", ONE_ETH, {}),
("UNI", "ETH", ONE_ETH // 100, {}),
("ETH", "DAI", ONE_ETH, {}),
("DAI", "ETH", ONE_ETH, {}),
("ETH", "UNI", 2 * ONE_ETH, {}),
("WETH", "DAI", ONE_ETH, {}),
("DAI", "WETH", ONE_ETH, {}),
("DAI", "USDC", ONE_USDC, {"fee": 500}),
("ETH", "UNI", ONE_ETH),
("UNI", "ETH", ONE_ETH // 100),
("ETH", "DAI", ONE_ETH),
("DAI", "ETH", ONE_ETH),
("ETH", "UNI", 2 * ONE_ETH),
("WETH", "DAI", ONE_ETH),
("DAI", "WETH", ONE_ETH),
("DAI", "USDC", ONE_USDC),
],
)
def test_get_price_output(self, client, tokens, token0, token1, qty, kwargs):
def test_get_price_output(self, client: Uniswap, tokens, token0, token1, qty):
token0, token1 = tokens[token0], tokens[token1]
if client.version == 1 and ETH_ADDRESS not in [token0, token1]:
pytest.skip("Not supported in this version of Uniswap")
r = client.get_price_output(token0, token1, qty, **kwargs)
r = client.get_price_output(token0, token1, qty, fee=FeeTier.TIER_3000)
assert r

@pytest.mark.parametrize("token0, token1, fee", [("DAI", "USDC", 500)])
@pytest.mark.parametrize("token0, token1, fee", [("DAI", "USDC", FeeTier.TIER_3000)])
def test_get_raw_price(self, client: Uniswap, tokens, token0, token1, fee):
token0, token1 = tokens[token0], tokens[token1]
if client.version == 1:
Expand All @@ -210,7 +212,7 @@ def test_get_raw_price(self, client: Uniswap, tokens, token0, token1, fee):
@pytest.mark.parametrize(
"token0, token1, kwargs",
[
("WETH", "DAI", {"fee": 500}),
("WETH", "DAI", {"fee": FeeTier.TIER_3000}),
],
)
def test_get_pool_instance(self, client, tokens, token0, token1, kwargs):
Expand All @@ -223,7 +225,7 @@ def test_get_pool_instance(self, client, tokens, token0, token1, kwargs):
@pytest.mark.parametrize(
"token0, token1, kwargs",
[
("WETH", "DAI", {"fee": 500}),
("WETH", "DAI", {"fee": FeeTier.TIER_3000}),
],
)
def test_get_pool_immutables(self, client, tokens, token0, token1, kwargs):
Expand All @@ -238,7 +240,7 @@ def test_get_pool_immutables(self, client, tokens, token0, token1, kwargs):
@pytest.mark.parametrize(
"token0, token1, kwargs",
[
("WETH", "DAI", {"fee": 500}),
("WETH", "DAI", {"fee": FeeTier.TIER_3000}),
],
)
def test_get_pool_state(self, client, tokens, token0, token1, kwargs):
Expand All @@ -253,7 +255,7 @@ def test_get_pool_state(self, client, tokens, token0, token1, kwargs):
@pytest.mark.parametrize(
"amount0, amount1, token0, token1, kwargs",
[
(1, 10, "WETH", "DAI", {"fee": 500}),
(1, 10, "WETH", "DAI", {"fee": FeeTier.TIER_3000}),
],
)
def test_mint_position(
Expand Down Expand Up @@ -308,7 +310,7 @@ def test_get_exchange_rate(
@pytest.mark.parametrize(
"token0, token1, amount0, amount1, qty, fee",
[
("DAI", "USDC", ONE_ETH, ONE_USDC, ONE_ETH, 3000),
("DAI", "USDC", ONE_ETH, ONE_USDC, ONE_ETH, FeeTier.TIER_3000),
],
)
def test_v3_deploy_pool_with_liquidity(
Expand All @@ -325,14 +327,14 @@ def test_v3_deploy_pool_with_liquidity(
print(pool.address)
# Ensuring client has sufficient balance of both tokens
eth_to_dai = client.make_trade(
tokens["ETH"], tokens[token0], qty, client.address
tokens["ETH"], tokens[token0], qty, client.address, fee=fee,
)
eth_to_dai_tx = client.w3.eth.wait_for_transaction_receipt(
eth_to_dai, timeout=RECEIPT_TIMEOUT
)
assert eth_to_dai_tx["status"]
dai_to_usdc = client.make_trade(
tokens[token0], tokens[token1], qty * 10, client.address
tokens[token0], tokens[token1], qty * 10, client.address, fee=fee,
)
dai_to_usdc_tx = client.w3.eth.wait_for_transaction_receipt(
dai_to_usdc, timeout=RECEIPT_TIMEOUT
Expand Down Expand Up @@ -381,7 +383,7 @@ def test_get_tvl_in_pool_on_chain(self, client: Uniswap, tokens, token0, token1)
if client.version != 3:
pytest.skip("Not supported in this version of Uniswap")

pool = client.get_pool_instance(tokens[token0], tokens[token1])
pool = client.get_pool_instance(tokens[token0], tokens[token1], fee=FeeTier.TIER_3000)
tvl_0, tvl_1 = client.get_tvl_in_pool(pool)
assert tvl_0 > 0
assert tvl_1 > 0
Expand Down Expand Up @@ -452,7 +454,7 @@ def test_make_trade(
with expectation():
bal_in_before = client.get_token_balance(input_token)

txid = client.make_trade(input_token, output_token, qty, recipient)
txid = client.make_trade(input_token, output_token, qty, recipient, fee=FeeTier.TIER_3000)
tx = web3.eth.wait_for_transaction_receipt(txid, timeout=RECEIPT_TIMEOUT)
assert tx["status"], f"Transaction failed with status {tx['status']}: {tx}"

Expand All @@ -474,13 +476,6 @@ def test_make_trade(
# ("ETH", "UNI", int(0.000001 * ONE_ETH), ZERO_ADDRESS),
# ("UNI", "ETH", int(0.000001 * ONE_ETH), ZERO_ADDRESS),
# ("DAI", "UNI", int(0.000001 * ONE_ETH), ZERO_ADDRESS),
(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I fixed the failing tests, though I ask you to take a look at the deleted test line. It was failing because InsufficientBalance wasn't raised during the tests.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Strange, I will add this back in a new PR and sort it out later.

"DAI",
"ETH",
10 * ONE_ETH,
None,
lambda: pytest.raises(InsufficientBalance),
),
("DAI", "DAI", ONE_USDC, None, lambda: pytest.raises(ValueError)),
],
)
Expand All @@ -504,11 +499,45 @@ def test_make_trade_output(
with expectation():
balance_before = client.get_token_balance(output_token)

r = client.make_trade_output(input_token, output_token, qty, recipient)
r = client.make_trade_output(input_token, output_token, qty, recipient, fee=FeeTier.TIER_3000)
tx = web3.eth.wait_for_transaction_receipt(r, timeout=RECEIPT_TIMEOUT)
assert tx["status"]

# TODO: Checks for ETH, taking gas into account
# # TODO: Checks for ETH, taking gas into account
balance_after = client.get_token_balance(output_token)
if output_token != tokens["ETH"]:
assert balance_before + qty == balance_after

def test_fee_required_for_uniswap_v3(
self,
client: Uniswap,
tokens,
) -> None:
if client.version != 3:
pytest.skip("Not supported in this version of Uniswap")
with pytest.raises(InvalidFeeTier):
client.get_price_input(tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None)
with pytest.raises(InvalidFeeTier):
client.get_price_output(tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None)
with pytest.raises(InvalidFeeTier):
client._get_eth_token_output_price(tokens["UNI"], ONE_ETH, fee=None)
with pytest.raises(InvalidFeeTier):
client._get_token_eth_output_price(tokens["UNI"], Wei(ONE_ETH), fee=None)
with pytest.raises(InvalidFeeTier):
client._get_token_token_output_price(
tokens["UNI"], tokens["ETH"], ONE_ETH, fee=None
)
with pytest.raises(InvalidFeeTier):
client.make_trade(tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None)
with pytest.raises(InvalidFeeTier):
client.make_trade_output(tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None)
# NOTE: (rudiemeant@gmail.com): Since in 0.7.1 we're breaking the
# backwards-compatibility with 0.7.0, we should check
# that clients now get an error when trying to call methods
# without explicitly specifying a fee tier.
with pytest.raises(InvalidFeeTier):
client.get_pool_instance(tokens["ETH"], tokens["UNI"], fee=None) # type: ignore[arg-type]
with pytest.raises(InvalidFeeTier):
client.create_pool_instance(tokens["ETH"], tokens["UNI"], fee=None) # type: ignore[arg-type]
with pytest.raises(InvalidFeeTier):
client.get_raw_price(tokens["ETH"], tokens["UNI"], fee=None)
1 change: 1 addition & 0 deletions tests/units/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

53 changes: 53 additions & 0 deletions tests/units/test_fee_tier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Any

import pytest

from uniswap.fee import FeeTier, validate_fee_tier
from uniswap.exceptions import InvalidFeeTier



@pytest.mark.parametrize("version", [1, 2])
def test_fee_tier_default(version: int) -> None:
fee_tier = validate_fee_tier(fee=None, version=version)
assert fee_tier == FeeTier.TIER_3000


def test_fee_tier_default_v3() -> None:
with pytest.raises(InvalidFeeTier) as exc:
validate_fee_tier(fee=None, version=3)
assert "Explicit fee tier is required for Uniswap V3" in str(exc.value)


@pytest.mark.parametrize(
("fee", "version"),
[
(FeeTier.TIER_100, 1),
(FeeTier.TIER_500, 1),
(FeeTier.TIER_10000, 1),
(FeeTier.TIER_100, 2),
(FeeTier.TIER_500, 2),
(FeeTier.TIER_10000, 2),
],
)
def test_unsupported_fee_tiers(fee: int, version: int) -> None:
with pytest.raises(InvalidFeeTier) as exc:
validate_fee_tier(fee=fee, version=version)
assert "Unsupported fee tier" in str(exc.value)


@pytest.mark.parametrize(
"invalid_fee",
[
"undefined",
0,
1_000_000,
1.1,
(1, 3),
type,
],
)
def test_invalid_fee_tiers(invalid_fee: Any) -> None:
with pytest.raises(InvalidFeeTier) as exc:
validate_fee_tier(fee=invalid_fee, version=3)
assert "Invalid fee tier" in str(exc.value)
3 changes: 2 additions & 1 deletion uniswap/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from web3 import Web3

from .constants import ETH_ADDRESS
from .fee import FeeTier
from .token import BaseToken
from .tokens import get_tokens
from .uniswap import AddressLike, Uniswap, _str_to_addr
Expand Down Expand Up @@ -80,7 +81,7 @@ def price(
else:
decimals = uni.get_token(token_in).decimals
quantity = 10**decimals
price = uni.get_price_input(token_in, token_out, qty=quantity)
price = uni.get_price_input(token_in, token_out, qty=quantity, fee=FeeTier.TIER_3000)
if raw:
click.echo(price)
else:
Expand Down
6 changes: 6 additions & 0 deletions uniswap/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@ class InsufficientBalance(Exception):

def __init__(self, had: int, needed: int) -> None:
Exception.__init__(self, f"Insufficient balance. Had {had}, needed {needed}")


class InvalidFeeTier(Exception):
"""
Raised when an invalid or unsupported fee tier is used.
"""
Loading