From ad8c8a14d7539c615795174ef0bcd52f6f73c614 Mon Sep 17 00:00:00 2001 From: jibranbinsaleem <40163456+jibranbinsaleem@users.noreply.github.com> Date: Sun, 10 Sep 2023 13:48:23 +0500 Subject: [PATCH 1/9] fix: add sepolia testnet constants (#347) --- uniswap/constants.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/uniswap/constants.py b/uniswap/constants.py index c66bae6..b30e9ab 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -31,6 +31,7 @@ 421611: "arbitrum_testnet", 1666600000: "harmony_mainnet", 1666700000: "harmony_testnet", + 11155111: "sepolia" } _factory_contract_addresses_v1 = { @@ -55,6 +56,7 @@ # SushiSwap on Harmony "harmony_mainnet": "0xc35DADB65012eC5796536bD9864eD8773aBc74C4", "harmony_testnet": "0xc35DADB65012eC5796536bD9864eD8773aBc74C4", + "sepolia": "0x7E0987E5b3a30e3f2828572Bb659A548460a3003" } _router_contract_addresses_v2 = { @@ -68,6 +70,8 @@ # SushiSwap on Harmony "harmony_mainnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", "harmony_testnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", + #sepolia tesnet router address + "sepolia": "0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008", } MAX_UINT_128 = (2**128) - 1 From cd5d95e8b4ebf89a9ce4fcc971a31e4c35668ca6 Mon Sep 17 00:00:00 2001 From: Anthony Date: Wed, 6 Dec 2023 17:16:13 +0400 Subject: [PATCH 2/9] fix: make fee a required argument for Uniswap V3 (#358) * Add FeeTier * Add FeeTier tests * Update project version 0.7.0 -> 0.7.1 * Revert several unnecessary changes * Fix tests --- README.md | 9 +++ pyproject.toml | 2 +- tests/test_uniswap.py | 119 ++++++++++++++++++++++------------- tests/units/__init__.py | 1 + tests/units/test_fee_tier.py | 53 ++++++++++++++++ uniswap/cli.py | 3 +- uniswap/exceptions.py | 6 ++ uniswap/fee.py | 52 +++++++++++++++ uniswap/uniswap.py | 46 ++++---------- 9 files changed, 209 insertions(+), 82 deletions(-) create mode 100644 tests/units/__init__.py create mode 100644 tests/units/test_fee_tier.py create mode 100644 uniswap/fee.py diff --git a/README.md b/README.md index 5fda097..935e541 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 3f7660d..378a706 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" diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 1e1a4b6..526921f 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -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, @@ -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,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) \ No newline at end of file diff --git a/tests/units/__init__.py b/tests/units/__init__.py new file mode 100644 index 0000000..5f28270 --- /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 0000000..0b404d8 --- /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) diff --git a/uniswap/cli.py b/uniswap/cli.py index 81c547a..78302cf 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: diff --git a/uniswap/exceptions.py b/uniswap/exceptions.py index 8080b77..efbc244 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 or unsupported fee tier is used. + """ diff --git a/uniswap/fee.py b/uniswap/fee.py new file mode 100644 index 0000000..0005d84 --- /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 0bb9ac0..5edbc85 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -44,6 +44,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 +235,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 +253,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 +355,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 +363,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 +376,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 +384,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 +405,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 +421,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 +453,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 +491,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 +1640,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 +1661,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 +1810,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 49573cd0b7710bbc985c85cc7449067334810f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 6 Dec 2023 14:19:51 +0100 Subject: [PATCH 3/9] docs: corrected changelog --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 935e541..d2034d5 100644 --- a/README.md +++ b/README.md @@ -80,11 +80,15 @@ Contributors also earn this beautiful [GitPOAP](https://gitpoap.notion.site/What ## Changelog -_0.7.1_ +_0.7.2_ * 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.1_ + +* incomplete changelog + _0.7.0_ * incomplete changelog From 3a63d337b5cfd84336310b64397b2b7449f06ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 6 Dec 2023 14:20:04 +0100 Subject: [PATCH 4/9] chore: bumped version to 0.7.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 378a706..3eb7978 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "uniswap-python" -version = "0.7.1" +version = "0.7.2" description = "An unofficial Python wrapper for the decentralized exchange Uniswap" repository = "https://github.com/shanefontaine/uniswap-python" readme = "README.md" From 59fba764cc2c1867990c12f2c56f79a8346b0643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 6 Dec 2023 14:33:55 +0100 Subject: [PATCH 5/9] fix: commented version field in pyproject.toml, mentioning it being auto-set on tagged releases in CI --- .github/workflows/release.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e55b0aa..086e263 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,5 +26,5 @@ jobs: # set pyproject.toml version to github.ref_name (without v prefix) # just in case someone forgot... VERSION=$(echo "${{ github.ref_name }}" | sed 's/^v//') - sed -i 's/^version = ".*"$/version = "'"$VERSION"'"/' pyproject.toml + sed -i 's/^version = ".*"/version = "'"$VERSION"'"/' pyproject.toml poetry publish --build --username=__token__ --password=$PYPI_TOKEN diff --git a/pyproject.toml b/pyproject.toml index 3eb7978..ecaf52a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "uniswap-python" -version = "0.7.2" +version = "0.7.2" # this is automatically set in CI on tagged releases (before pushed to PyPI) description = "An unofficial Python wrapper for the decentralized exchange Uniswap" repository = "https://github.com/shanefontaine/uniswap-python" readme = "README.md" From 7c1d83f81ed5cbdda774df43bfc72420e6512dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 1 May 2024 12:58:50 +0200 Subject: [PATCH 6/9] nit: fixed typo --- uniswap/uniswap.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 5edbc85..183c349 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1403,7 +1403,7 @@ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> Non tx = self._build_and_send_tx(function) self.w3.eth.wait_for_transaction_receipt(tx, timeout=6000) - # Add extra sleep to let tx propogate correctly + # Add extra sleep to let tx propagate correctly time.sleep(1) def _is_approved(self, token: AddressLike) -> bool: @@ -1413,6 +1413,8 @@ def _is_approved(self, token: AddressLike) -> bool: contract_addr = self._exchange_address_from_token(token) elif self.version in [2, 3]: contract_addr = self.router_address + else: + raise ValueError amount = ( _load_contract_erc20(self.w3, token) .functions.allowance(self.address, contract_addr) From 7f4d5c740fefd8355c2837ee2e7e3c6f0c455980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Wed, 22 May 2024 15:49:06 +0200 Subject: [PATCH 7/9] fix: added get_tick_at_sqrt util function --- uniswap/util.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/uniswap/util.py b/uniswap/util.py index 814ec9d..63d5147 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -99,6 +99,32 @@ def encode_sqrt_ratioX96(amount_0: int, amount_1: int) -> int: return int(math.sqrt(ratioX192)) +def get_tick_at_sqrt(sqrtPriceX96: int) -> int: + sqrtPriceX96 = int(sqrtPriceX96) + + # Define constants + Q96 = 2**96 + + # Calculate the price from the sqrt ratio + ratio = sqrtPriceX96 / Q96 + price = ratio**2 + + # Calculate the natural logarithm of the price + logPrice = math.log(price) + + # Calculate the log base 1.0001 of the price + logBase = math.log(1.0001) + tick = logPrice / logBase + + # Round tick to nearest integer + tick = int(round(tick)) + + # Ensure the tick is within the valid range + assert tick >= MIN_TICK and tick <= MAX_TICK + + return tick + + # Adapted from: https://github.com/tradingstrategy-ai/web3-ethereum-defi/blob/c3c68bc723d55dda0cc8252a0dadb534c4fdb2c5/eth_defi/uniswap_v3/utils.py#L77 def get_min_tick(fee: int) -> int: min_tick_spacing: int = _tick_spacing[fee] From 4f15d2ef7e9d7cb502e6b2ce44772fae46d18781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Thu, 23 May 2024 13:54:12 +0200 Subject: [PATCH 8/9] fix: added decode_sqrt_ratioX96 util function --- uniswap/util.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/uniswap/util.py b/uniswap/util.py index 63d5147..888aa39 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -99,6 +99,13 @@ def encode_sqrt_ratioX96(amount_0: int, amount_1: int) -> int: return int(math.sqrt(ratioX192)) +def decode_sqrt_ratioX96(sqrtPriceX96: int) -> float: + Q96 = 2**96 + ratio = sqrtPriceX96 / Q96 + price = ratio**2 + return price + + def get_tick_at_sqrt(sqrtPriceX96: int) -> int: sqrtPriceX96 = int(sqrtPriceX96) From f5a9e8668717bd1ba3f63bc8342bbc9e93b55121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Fri, 18 Oct 2024 12:45:46 +0200 Subject: [PATCH 9/9] fix: cleaned up sepolia address --- uniswap/constants.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/uniswap/constants.py b/uniswap/constants.py index b30e9ab..5eca633 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 = { @@ -64,14 +64,13 @@ "ropsten": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", "rinkeby": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", "görli": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + "sepolia": "0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008", "xdai": "0x1C232F01118CB8B424793ae03F870aa7D0ac7f77", "binance": "0x10ED43C718714eb63d5aA57B78B54704E256024E", "binance_testnet": "0xD99D1c33F9fC3444f8101754aBC46c52416550D1", # SushiSwap on Harmony "harmony_mainnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", "harmony_testnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", - #sepolia tesnet router address - "sepolia": "0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008", } MAX_UINT_128 = (2**128) - 1 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy