From 7a908112a64bae831f28fc3e609a7fe6db23a1c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Fri, 16 Jun 2023 11:48:50 +0200 Subject: [PATCH 1/6] docs(README): added notice about upcoming v4 support --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c9c2fb7a..5fda0975 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ The unofficial Python client for [Uniswap](https://uniswap.io/). Documentation is available at https://uniswap-python.com/ +**Want to help implement support for Uniswap v4?** See [issue #337](https://github.com/uniswap-python/uniswap-python/issues/337) + ## Functionality * A simple to use Python wrapper for all available contract functions and variables From 9a1318f538f7ff28fceedb5c183a48ecb3bfba5b Mon Sep 17 00:00:00 2001 From: voyz Date: Fri, 8 Sep 2023 03:46:08 -0500 Subject: [PATCH 2/6] feat: price impact excludes fees (#325) * feat: added util functions for fee calculations * feat: price impact calculation now excludes fees * removed unused 'fraction_to_fee' and reformatted with black --- examples/price_impact.py | 8 ++++---- tests/test_uniswap.py | 8 ++++++++ uniswap/constants.py | 9 +++++++-- uniswap/uniswap.py | 10 +++++++++- uniswap/util.py | 16 ++++++++++++++++ 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/examples/price_impact.py b/examples/price_impact.py index 15c3a0a6..e9c99c99 100644 --- a/examples/price_impact.py +++ b/examples/price_impact.py @@ -27,7 +27,7 @@ def usdt_to_vxv_v2(): # Compare the results with the output of: # https://app.uniswap.org/#/swap?use=v2&inputCurrency=0xdac17f958d2ee523a2206206994597c13d831ec7&outputCurrency=0x7d29a64504629172a429e64183d6673b9dacbfce - qty = 10 * 10 ** 8 + qty = 10 * 10**8 # price = uniswap.get_price_input(usdt, vxv, qty, route=route) / 10 ** 18 # print(price) @@ -38,7 +38,7 @@ def usdt_to_vxv_v2(): # The slippage for v3 (in example below) returns correct results. print(f"Impact for buying VXV on v2 with {qty / 10**8} USDT: {_perc(impact)}") - qty = 13900 * 10 ** 8 + qty = 13900 * 10**8 impact = uniswap.estimate_price_impact(usdt, vxv, qty, route=route) print(f"Impact for buying VXV on v2 with {qty / 10**8} USDT: {_perc(impact)}") @@ -49,11 +49,11 @@ def eth_to_vxv_v3(): # Compare the results with the output of: # https://app.uniswap.org/#/swap?use=v3&inputCurrency=ETH&outputCurrency=0x7d29a64504629172a429e64183d6673b9dacbfce - qty = 1 * 10 ** 18 + qty = 1 * 10**18 impact = uniswap.estimate_price_impact(eth, vxv, qty, fee=10000) print(f"Impact for buying VXV on v3 with {qty / 10**18} ETH: {_perc(impact)}") - qty = 100 * 10 ** 18 + qty = 100 * 10**18 impact = uniswap.estimate_price_impact(eth, vxv, qty, fee=10000) print(f"Impact for buying VXV on v3 with {qty / 10**18} ETH: {_perc(impact)}") diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 12ba29c7..1e1a4b6f 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -69,6 +69,7 @@ def test_assets(client: Uniswap): """ tokens = get_tokens(client.netname) + for token_name, amount in [ ("DAI", 10_000 * ONE_DAI), ("USDC", 10_000 * ONE_USDC), @@ -133,6 +134,13 @@ def does_not_raise(): yield + +ONE_ETH = 10**18 +ONE_USDC = 10**6 + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + # TODO: Change pytest.param(..., mark=pytest.mark.xfail) to the expectation/raises method @pytest.mark.usefixtures("client", "web3") class TestUniswap(object): diff --git a/uniswap/constants.py b/uniswap/constants.py index 12959a72..de6d06a5 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -78,7 +78,12 @@ MAX_TICK = -MIN_TICK # Source: https://github.com/Uniswap/v3-core/blob/v1.0.0/contracts/UniswapV3Factory.sol#L26-L31 -_tick_spacing = {100:1, 500: 10, 3_000: 60, 10_000: 200} +_tick_spacing = {100: 1, 500: 10, 3_000: 60, 10_000: 200} # Derived from (MIN_TICK//tick_spacing) >> 8 and (MAX_TICK//tick_spacing) >> 8 -_tick_bitmap_range = {100:(-3466, 3465), 500: (-347, 346), 3_000: (-58, 57), 10_000: (-18, 17)} +_tick_bitmap_range = { + 100: (-3466, 3465), + 500: (-347, 346), + 3_000: (-58, 57), + 10_000: (-18, 17), +} diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index e517eea3..47a97acd 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -33,6 +33,7 @@ encode_sqrt_ratioX96, is_same_address, nearest_tick, + realised_fee_percentage, ) from .decorators import supports, check_approval from .constants import ( @@ -1926,7 +1927,14 @@ def estimate_price_impact( cost_amount / (amount_in / (10 ** self.get_token(token_in).decimals)) ) / 10 ** self.get_token(token_out).decimals - return float((price_small - price_amount) / price_small) + # calculate and subtract the realised fees from the price impact. See: + # https://github.com/uniswap-python/uniswap-python/issues/310 + # The fee calculation will need to be updated when adding support for the AutoRouter. + price_impact_with_fees = float((price_small - price_amount) / price_small) + fee_realised_percentage = realised_fee_percentage(fee, amount_in) + price_impact_real = price_impact_with_fees - fee_realised_percentage + + return price_impact_real # ------ Exchange ------------------------------------------------------------------ @supports([1, 2]) diff --git a/uniswap/util.py b/uniswap/util.py index 761e6347..6cb7d4a2 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -126,3 +126,19 @@ def nearest_tick(tick: int, fee: int) -> int: def chunks(arr: Sequence[Any], n: int) -> Generator: for i in range(0, len(arr), n): yield arr[i : i + n] + + +def fee_to_fraction(fee: int): + return fee / 1000000 + + +def realised_fee_percentage(fee: int, amount_in: int) -> float: + """ + Calculate realised fee expressed as a percentage of the amount_in. + The realised fee is rounded up as fractional units cannot be used - + this correlates to how the fees are rounded by Uniswap. + """ + + fee_percentage = fee_to_fraction(fee) + fee_realised = math.ceil(amount_in * fee_percentage) + return fee_realised / amount_in From c81e84c77f4d441dc4bd6646fc745cf8d8d76bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sat, 9 Sep 2023 14:15:21 +0200 Subject: [PATCH 3/6] fix: fixed typing --- uniswap/uniswap.py | 2 +- uniswap/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 47a97acd..b3ff3c89 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1891,7 +1891,7 @@ def estimate_price_impact( token_in: AddressLike, token_out: AddressLike, amount_in: int, - fee: Optional[int] = None, + fee: int, route: Optional[List[AddressLike]] = None, ) -> float: """ diff --git a/uniswap/util.py b/uniswap/util.py index 6cb7d4a2..9cf64a9f 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -128,7 +128,7 @@ def chunks(arr: Sequence[Any], n: int) -> Generator: yield arr[i : i + n] -def fee_to_fraction(fee: int): +def fee_to_fraction(fee: int) -> float: return fee / 1000000 From 2029495b4e37767feaa13da170a8b9fbaa348da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sat, 9 Sep 2023 14:16:39 +0200 Subject: [PATCH 4/6] style: sorted imports --- uniswap/cli.py | 7 +++-- uniswap/constants.py | 5 ++-- uniswap/decorators.py | 13 ++++++--- uniswap/token.py | 1 + uniswap/tokens.py | 3 +-- uniswap/uniswap.py | 62 +++++++++++++++++++++++++------------------ uniswap/util.py | 27 +++++++++++++------ 7 files changed, 72 insertions(+), 46 deletions(-) diff --git a/uniswap/cli.py b/uniswap/cli.py index b177712c..81c547ad 100644 --- a/uniswap/cli.py +++ b/uniswap/cli.py @@ -1,16 +1,15 @@ import logging import os +from typing import Optional import click from dotenv import load_dotenv from web3 import Web3 -from typing import Optional -from .uniswap import Uniswap, AddressLike, _str_to_addr +from .constants import ETH_ADDRESS from .token import BaseToken from .tokens import get_tokens -from .constants import ETH_ADDRESS - +from .uniswap import AddressLike, Uniswap, _str_to_addr logger = logging.getLogger(__name__) diff --git a/uniswap/constants.py b/uniswap/constants.py index de6d06a5..c66bae66 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -1,7 +1,6 @@ from typing import Set, cast -from web3.types import ( # noqa: F401 - RPCEndpoint, -) + +from web3.types import RPCEndpoint # noqa: F401 # look at web3/middleware/cache.py for reference # RPC methods that will be cached inside _get_eth_simple_cache_middleware diff --git a/uniswap/decorators.py b/uniswap/decorators.py index da3ba4f3..dfced8f6 100644 --- a/uniswap/decorators.py +++ b/uniswap/decorators.py @@ -1,9 +1,16 @@ import functools -from typing import Callable, List, TYPE_CHECKING, TypeVar, Optional -from typing_extensions import ParamSpec, Concatenate +from typing import ( + TYPE_CHECKING, + Callable, + List, + Optional, + TypeVar, +) + +from typing_extensions import Concatenate, ParamSpec -from .types import AddressLike from .constants import ETH_ADDRESS +from .types import AddressLike if TYPE_CHECKING: from .uniswap import Uniswap diff --git a/uniswap/token.py b/uniswap/token.py index bcbeaf54..5f72c51f 100644 --- a/uniswap/token.py +++ b/uniswap/token.py @@ -1,4 +1,5 @@ from dataclasses import dataclass + from .types import AddressLike diff --git a/uniswap/tokens.py b/uniswap/tokens.py index 9870db0a..6008e644 100644 --- a/uniswap/tokens.py +++ b/uniswap/tokens.py @@ -1,8 +1,7 @@ from typing import Dict -from web3 import Web3 from eth_typing.evm import ChecksumAddress - +from web3 import Web3 tokens_mainnet: Dict[str, ChecksumAddress] = { k: Web3.to_checksum_address(v) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index b3ff3c89..0bb9ac0d 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1,10 +1,21 @@ -from collections import namedtuple +import functools +import logging import os import time -import logging -import functools -from typing import List, Any, Optional, Sequence, Union, Tuple, Iterable, Dict +from collections import namedtuple +from typing import ( + Any, + Dict, + Iterable, + List, + Optional, + Sequence, + Tuple, + Union, +) +from eth_typing.evm import Address, ChecksumAddress +from hexbytes import HexBytes from web3 import Web3 from web3._utils.abi import map_abi_data from web3._utils.normalizers import BASE_RETURN_NORMALIZERS @@ -12,43 +23,42 @@ from web3.contract.contract import ContractFunction from web3.exceptions import BadFunctionCallOutput, ContractLogicError from web3.types import ( + Nonce, TxParams, TxReceipt, Wei, - Nonce, ) -from eth_typing.evm import Address, ChecksumAddress -from hexbytes import HexBytes -from .types import AddressLike + +from .constants import ( + ETH_ADDRESS, + MAX_TICK, + MAX_UINT_128, + MIN_TICK, + WETH9_ADDRESS, + _factory_contract_addresses_v1, + _factory_contract_addresses_v2, + _netid_to_name, + _router_contract_addresses_v2, + _tick_bitmap_range, + _tick_spacing, +) +from .decorators import check_approval, supports +from .exceptions import InsufficientBalance, InvalidToken from .token import ERC20Token -from .exceptions import InvalidToken, InsufficientBalance +from .types import AddressLike from .util import ( - _get_eth_simple_cache_middleware, - _str_to_addr, _addr_to_str, - _validate_address, + _get_eth_simple_cache_middleware, _load_contract, _load_contract_erc20, + _str_to_addr, + _validate_address, chunks, encode_sqrt_ratioX96, is_same_address, nearest_tick, realised_fee_percentage, ) -from .decorators import supports, check_approval -from .constants import ( - MAX_UINT_128, - MAX_TICK, - MIN_TICK, - WETH9_ADDRESS, - _netid_to_name, - _factory_contract_addresses_v1, - _factory_contract_addresses_v2, - _router_contract_addresses_v2, - _tick_spacing, - _tick_bitmap_range, - ETH_ADDRESS, -) logger = logging.getLogger(__name__) diff --git a/uniswap/util.py b/uniswap/util.py index 9cf64a9f..814ec9d7 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -1,19 +1,30 @@ -import os +import functools import json import math -import functools -import lru - -from typing import Any, Generator, List, Sequence, Tuple, Union +import os +from typing import ( + Any, + Generator, + List, + Sequence, + Tuple, + Union, +) +import lru from web3 import Web3 -from web3.exceptions import NameNotFound from web3.contract import Contract +from web3.exceptions import NameNotFound from web3.middleware.cache import construct_simple_cache_middleware from web3.types import Middleware -from .constants import MIN_TICK, MAX_TICK, _tick_spacing, SIMPLE_CACHE_RPC_WHITELIST -from .types import AddressLike, Address +from .constants import ( + MAX_TICK, + MIN_TICK, + SIMPLE_CACHE_RPC_WHITELIST, + _tick_spacing, +) +from .types import Address, AddressLike def _get_eth_simple_cache_middleware() -> Middleware: From 62c4ac413ff78f6085b5435257fdf308a847df90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sat, 9 Sep 2023 14:16:56 +0200 Subject: [PATCH 5/6] fix: added __all__ to __init__ --- uniswap/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/uniswap/__init__.py b/uniswap/__init__.py index 53d0a5bb..a1612f65 100644 --- a/uniswap/__init__.py +++ b/uniswap/__init__.py @@ -1,3 +1,5 @@ from . import exceptions -from .uniswap import Uniswap, _str_to_addr from .cli import main +from .uniswap import Uniswap, _str_to_addr + +__all__ = ["Uniswap", "exceptions", "_str_to_addr", "main"] From fa46b8099a89a8fd399fcc0a3ad2db87d3dd9703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Sat, 9 Sep 2023 14:18:51 +0200 Subject: [PATCH 6/6] ci: added release CI (uploads to PyPi) --- .github/workflows/release.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..e55b0aaa --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,30 @@ +name: Release + +on: + release: + types: [created] + +jobs: + publish-pypi: + name: Publish to PyPi + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install poetry + - name: Build and publish + env: + PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} + run: | + # 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 + poetry publish --build --username=__token__ --password=$PYPI_TOKEN