diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e55b0aa --- /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 diff --git a/README.md b/README.md index c9c2fb7..5fda097 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 diff --git a/examples/price_impact.py b/examples/price_impact.py index 15c3a0a..e9c99c9 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 12ba29c..1e1a4b6 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/__init__.py b/uniswap/__init__.py index 53d0a5b..a1612f6 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"] diff --git a/uniswap/cli.py b/uniswap/cli.py index b177712..81c547a 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 12959a7..c66bae6 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 @@ -78,7 +77,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/decorators.py b/uniswap/decorators.py index da3ba4f..dfced8f 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 bcbeaf5..5f72c51 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 9870db0..6008e64 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 e517eea..0bb9ac0 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,41 +23,41 @@ 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, -) -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, + realised_fee_percentage, ) logger = logging.getLogger(__name__) @@ -1890,7 +1901,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: """ @@ -1926,7 +1937,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 761e634..814ec9d 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: @@ -126,3 +137,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) -> float: + 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
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: