From c707b70ba2aedb3373ca29ca2bff0cdbefda84d5 Mon Sep 17 00:00:00 2001 From: Anton Bagrianov Date: Mon, 4 Dec 2023 18:14:27 +0400 Subject: [PATCH 1/5] Add FeeTier --- uniswap/constants.py | 6 ++--- uniswap/exceptions.py | 6 +++++ uniswap/fee.py | 52 +++++++++++++++++++++++++++++++++++++++++++ uniswap/uniswap.py | 47 ++++++++++---------------------------- 4 files changed, 73 insertions(+), 38 deletions(-) create mode 100644 uniswap/fee.py diff --git a/uniswap/constants.py b/uniswap/constants.py index b30e9abf..06d4faf4 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -31,7 +31,7 @@ 421611: "arbitrum_testnet", 1666600000: "harmony_mainnet", 1666700000: "harmony_testnet", - 11155111: "sepolia" + 11155111: "sepolia", } _factory_contract_addresses_v1 = { @@ -56,7 +56,7 @@ # SushiSwap on Harmony "harmony_mainnet": "0xc35DADB65012eC5796536bD9864eD8773aBc74C4", "harmony_testnet": "0xc35DADB65012eC5796536bD9864eD8773aBc74C4", - "sepolia": "0x7E0987E5b3a30e3f2828572Bb659A548460a3003" + "sepolia": "0x7E0987E5b3a30e3f2828572Bb659A548460a3003", } _router_contract_addresses_v2 = { @@ -70,7 +70,7 @@ # SushiSwap on Harmony "harmony_mainnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", "harmony_testnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", - #sepolia tesnet router address + # sepolia tesnet router address "sepolia": "0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008", } diff --git a/uniswap/exceptions.py b/uniswap/exceptions.py index 8080b773..a89ae856 100644 --- a/uniswap/exceptions.py +++ b/uniswap/exceptions.py @@ -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 fee tier is used for a protocol that supports multiple fee tiers (Uniswap V3 currently). + """ diff --git a/uniswap/fee.py b/uniswap/fee.py new file mode 100644 index 00000000..0005d843 --- /dev/null +++ b/uniswap/fee.py @@ -0,0 +1,52 @@ +import enum +import logging +from typing import final, Final, Optional + +from .exceptions import InvalidFeeTier + +logger: Final = logging.getLogger(__name__) + + +@final +@enum.unique +class FeeTier(enum.IntEnum): + """ + Available fee tiers represented as 1e-6 percentages (i.e. 0.5% is 5000) + + V1 supports only 0.3% fee tier. + V2 supports only 0.3% fee tier. + V3 supports 1%, 0.3%, 0.05%, and 0.01% fee tiers. + + Reference: https://support.uniswap.org/hc/en-us/articles/20904283758349-What-are-fee-tiers + """ + + TIER_100 = 100 + TIER_500 = 500 + TIER_3000 = 3000 + TIER_10000 = 10000 + + +def validate_fee_tier(fee: Optional[int], version: int) -> int: + """ + Validate fee tier for a given Uniswap version. + """ + if version == 3 and fee is None: + raise InvalidFeeTier( + """ + Explicit fee tier is required for Uniswap V3. Refer to the following link for more information: + https://support.uniswap.org/hc/en-us/articles/20904283758349-What-are-fee-tiers + """ + ) + if fee is None: + fee = FeeTier.TIER_3000 + + if version < 3 and fee != FeeTier.TIER_3000: + raise InvalidFeeTier( + f"Unsupported fee tier {fee} for Uniswap V{version}. Choices are: {FeeTier.TIER_3000}" + ) + try: + return FeeTier(fee).value + except ValueError as exc: + raise InvalidFeeTier( + f"Invalid fee tier {fee} for Uniswap V{version}. Choices are: {FeeTier._value2member_map_.keys()}" + ) from exc diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 0bb9ac0d..a62717bc 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -29,6 +29,7 @@ Wei, ) + from .constants import ( ETH_ADDRESS, MAX_TICK, @@ -44,6 +45,7 @@ ) from .decorators import check_approval, supports from .exceptions import InsufficientBalance, InvalidToken +from .fee import validate_fee_tier from .token import ERC20Token from .types import AddressLike from .util import ( @@ -234,10 +236,7 @@ def get_price_input( route: Optional[List[AddressLike]] = None, ) -> int: """Given `qty` amount of the input `token0`, returns the maximum output amount of output `token1`.""" - if fee is None: - fee = 3000 - if self.version == 3: - logger.warning("No fee set, assuming 0.3%") + fee = validate_fee_tier(fee=fee, version=self.version) if token0 == ETH_ADDRESS: return self._get_eth_token_input_price(token1, Wei(qty), fee) @@ -255,10 +254,7 @@ def get_price_output( route: Optional[List[AddressLike]] = None, ) -> int: """Returns the minimum amount of `token0` required to buy `qty` amount of `token1`.""" - if fee is None: - fee = 3000 - if self.version == 3: - logger.warning("No fee set, assuming 0.3%") + fee = validate_fee_tier(fee=fee, version=self.version) if is_same_address(token0, ETH_ADDRESS): return self._get_eth_token_output_price(token1, qty, fee) @@ -360,6 +356,7 @@ def _get_eth_token_output_price( fee: Optional[int] = None, ) -> Wei: """Public price (i.e. amount of ETH needed) for ETH to token trades with an exact output.""" + fee = validate_fee_tier(fee=fee, version=self.version) if self.version == 1: ex = self._exchange_contract(token) price: Wei = ex.functions.getEthToTokenOutputPrice(qty).call() @@ -367,9 +364,6 @@ def _get_eth_token_output_price( route = [self.get_weth_address(), token] price = self.router.functions.getAmountsIn(qty, route).call()[0] elif self.version == 3: - if fee is None: - logger.warning("No fee set, assuming 0.3%") - fee = 3000 price = Wei( self._get_token_token_output_price( self.get_weth_address(), token, qty, fee=fee @@ -383,6 +377,7 @@ def _get_token_eth_output_price( self, token: AddressLike, qty: Wei, fee: Optional[int] = None # input token ) -> int: """Public price (i.e. amount of input token needed) for token to ETH trades with an exact output.""" + fee = validate_fee_tier(fee=fee, version=self.version) if self.version == 1: ex = self._exchange_contract(token) price: int = ex.functions.getTokenToEthOutputPrice(qty).call() @@ -390,9 +385,6 @@ def _get_token_eth_output_price( route = [token, self.get_weth_address()] price = self.router.functions.getAmountsIn(qty, route).call()[0] elif self.version == 3: - if not fee: - logger.warning("No fee set, assuming 0.3%") - fee = 3000 price = self._get_token_token_output_price( token, self.get_weth_address(), qty, fee=fee ) @@ -414,6 +406,7 @@ def _get_token_token_output_price( :param fee: (v3 only) The pool's fee in hundredths of a bip, i.e. 1e-6 (3000 is 0.3%) """ + fee = validate_fee_tier(fee=fee, version=self.version) if not route: if self.version == 2: # If one of the tokens are WETH, delegate to appropriate call. @@ -429,9 +422,6 @@ def _get_token_token_output_price( if self.version == 2: price: int = self.router.functions.getAmountsIn(qty, route).call()[0] elif self.version == 3: - if not fee: - logger.warning("No fee set, assuming 0.3%") - fee = 3000 if route: # NOTE: to support custom routes we need to support the Path data encoding: https://github.com/Uniswap/uniswap-v3-periphery/blob/main/contracts/libraries/Path.sol # result: tuple = self.quoter.functions.quoteExactOutput(route, qty).call() @@ -464,10 +454,7 @@ def make_trade( if not isinstance(qty, int): raise TypeError("swapped quantity must be an integer") - if fee is None: - fee = 3000 - if self.version == 3: - logger.warning("No fee set, assuming 0.3%") + fee = validate_fee_tier(fee=fee, version=self.version) if slippage is None: slippage = self.default_slippage @@ -505,10 +492,7 @@ def make_trade_output( slippage: Optional[float] = None, ) -> HexBytes: """Make a trade by defining the qty of the output token.""" - if fee is None: - fee = 3000 - if self.version == 3: - logger.warning("No fee set, assuming 0.3%") + fee = validate_fee_tier(fee=fee, version=self.version) if slippage is None: slippage = self.default_slippage @@ -1657,9 +1641,7 @@ def get_pool_instance( """ assert token_0 != token_1, "Token addresses cannot be the same" - assert fee in list( - _tick_spacing.keys() - ), "Uniswap V3 only supports three levels of fees: 0.05%, 0.3%, 1%" + fee = validate_fee_tier(fee=fee, version=self.version) pool_address = self.factory_contract.functions.getPool( token_0, token_1, fee @@ -1680,9 +1662,7 @@ def create_pool_instance( """ address = _addr_to_str(self.address) assert token_0 != token_1, "Token addresses cannot be the same" - assert fee in list( - _tick_spacing.keys() - ), "Uniswap V3 only supports three levels of fees: 0.05%, 0.3%, 1%" + fee = validate_fee_tier(fee=fee, version=self.version) tx = self.factory_contract.functions.createPool(token_0, token_1, fee).transact( {"from": address} @@ -1831,10 +1811,7 @@ def get_raw_price( Parameter `fee` is required for V3 only, can be omitted for V2 Requires pair [token_in, token_out] having direct pool """ - if not fee: - fee = 3000 - if self.version == 3: - logger.warning("No fee set, assuming 0.3%") + fee = validate_fee_tier(fee=fee, version=self.version) if token_in == ETH_ADDRESS: token_in = self.get_weth_address() From e18e01e505307f2b21130d6e08acdff0ed6596ae Mon Sep 17 00:00:00 2001 From: Anton Bagrianov Date: Mon, 4 Dec 2023 18:14:38 +0400 Subject: [PATCH 2/5] Add FeeTier tests --- tests/test_uniswap.py | 32 +++++++++++++++++++++- tests/units/__init__.py | 1 + tests/units/test_fee_tier.py | 53 ++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 tests/units/__init__.py create mode 100644 tests/units/test_fee_tier.py diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 1e1a4b6f..5221560b 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -12,7 +12,7 @@ from uniswap import Uniswap from uniswap.constants import ETH_ADDRESS -from uniswap.exceptions import InsufficientBalance +from uniswap.exceptions import InsufficientBalance, InvalidFeeTier from uniswap.tokens import get_tokens from uniswap.util import ( _str_to_addr, @@ -512,3 +512,33 @@ def test_make_trade_output( 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"], 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) + with pytest.raises(InvalidFeeTier): + client.get_pool_instance(tokens["ETH"], tokens["UNI"], fee=None) + with pytest.raises(InvalidFeeTier): + client.create_pool_instance(tokens["ETH"], tokens["UNI"], fee=None) + with pytest.raises(InvalidFeeTier): + client.get_raw_price(tokens["ETH"], tokens["UNI"], fee=None) \ No newline at end of file diff --git a/tests/units/__init__.py b/tests/units/__init__.py new file mode 100644 index 00000000..5f282702 --- /dev/null +++ b/tests/units/__init__.py @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/units/test_fee_tier.py b/tests/units/test_fee_tier.py new file mode 100644 index 00000000..0b404d89 --- /dev/null +++ b/tests/units/test_fee_tier.py @@ -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) From 0c9cd26d743939962a1f6c2c82a58f03599c0324 Mon Sep 17 00:00:00 2001 From: Anton Bagrianov Date: Mon, 4 Dec 2023 18:23:12 +0400 Subject: [PATCH 3/5] Update project version 0.7.0 -> 0.7.1 --- README.md | 9 +++++++++ pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5fda0975..935e5416 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/pyproject.toml b/pyproject.toml index 3f7660dc..378a7060 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" From 9b985f747540bba800c6b269f50e7b5ffe010e78 Mon Sep 17 00:00:00 2001 From: Anton Bagrianov Date: Mon, 4 Dec 2023 18:27:06 +0400 Subject: [PATCH 4/5] Revert several unnecessary changes --- uniswap/constants.py | 6 +++--- uniswap/exceptions.py | 2 +- uniswap/uniswap.py | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/uniswap/constants.py b/uniswap/constants.py index 06d4faf4..b30e9abf 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -31,7 +31,7 @@ 421611: "arbitrum_testnet", 1666600000: "harmony_mainnet", 1666700000: "harmony_testnet", - 11155111: "sepolia", + 11155111: "sepolia" } _factory_contract_addresses_v1 = { @@ -56,7 +56,7 @@ # SushiSwap on Harmony "harmony_mainnet": "0xc35DADB65012eC5796536bD9864eD8773aBc74C4", "harmony_testnet": "0xc35DADB65012eC5796536bD9864eD8773aBc74C4", - "sepolia": "0x7E0987E5b3a30e3f2828572Bb659A548460a3003", + "sepolia": "0x7E0987E5b3a30e3f2828572Bb659A548460a3003" } _router_contract_addresses_v2 = { @@ -70,7 +70,7 @@ # SushiSwap on Harmony "harmony_mainnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", "harmony_testnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", - # sepolia tesnet router address + #sepolia tesnet router address "sepolia": "0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008", } diff --git a/uniswap/exceptions.py b/uniswap/exceptions.py index a89ae856..efbc244c 100644 --- a/uniswap/exceptions.py +++ b/uniswap/exceptions.py @@ -17,5 +17,5 @@ def __init__(self, had: int, needed: int) -> None: class InvalidFeeTier(Exception): """ - Raised when an invalid fee tier is used for a protocol that supports multiple fee tiers (Uniswap V3 currently). + Raised when an invalid or unsupported fee tier is used. """ diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index a62717bc..5edbc857 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -29,7 +29,6 @@ Wei, ) - from .constants import ( ETH_ADDRESS, MAX_TICK, From eb744d7f3666146f52ba4a4c04580ffe8b85795e Mon Sep 17 00:00:00 2001 From: Anton Bagrianov Date: Tue, 5 Dec 2023 13:03:50 +0400 Subject: [PATCH 5/5] Fix tests --- tests/test_uniswap.py | 93 +++++++++++++++++++++---------------------- uniswap/cli.py | 3 +- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 5221560b..526921fc 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -9,9 +9,11 @@ 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.fee import FeeTier from uniswap.exceptions import InsufficientBalance, InvalidFeeTier from uniswap.tokens import get_tokens from uniswap.util import ( @@ -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}" @@ -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: @@ -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): @@ -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): @@ -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): @@ -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( @@ -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( @@ -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 @@ -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 @@ -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}" @@ -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), - ( - "DAI", - "ETH", - 10 * ONE_ETH, - None, - lambda: pytest.raises(InsufficientBalance), - ), ("DAI", "DAI", ONE_USDC, None, lambda: pytest.raises(ValueError)), ], ) @@ -504,11 +499,11 @@ 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 @@ -527,7 +522,7 @@ def test_fee_required_for_uniswap_v3( 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"], ONE_ETH, fee=None) + 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 @@ -536,9 +531,13 @@ def test_fee_required_for_uniswap_v3( 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) + 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) + 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) \ No newline at end of file diff --git a/uniswap/cli.py b/uniswap/cli.py index 81c547ad..78302cf5 100644 --- a/uniswap/cli.py +++ b/uniswap/cli.py @@ -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 @@ -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: