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/README.md b/README.md index 5fda097..d2034d5 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,19 @@ Contributors also earn this beautiful [GitPOAP](https://gitpoap.notion.site/What ## Changelog +_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 + _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..ecaf52a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "uniswap-python" -version = "0.7.0" +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" 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/constants.py b/uniswap/constants.py index c66bae6..5eca633 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 = { @@ -62,6 +64,7 @@ "ropsten": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", "rinkeby": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", "görli": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + "sepolia": "0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008", "xdai": "0x1C232F01118CB8B424793ae03F870aa7D0ac7f77", "binance": "0x10ED43C718714eb63d5aA57B78B54704E256024E", "binance_testnet": "0xD99D1c33F9fC3444f8101754aBC46c52416550D1", 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..183c349 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 @@ -1420,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: @@ -1430,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) @@ -1657,9 +1642,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 +1663,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 +1812,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() diff --git a/uniswap/util.py b/uniswap/util.py index 814ec9d..888aa39 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -99,6 +99,39 @@ 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) + + # 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]
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: