diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..7c8b5c14 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior. Ideally, you can provide a code example demonstrating the issue (including the Uniswap version, tokens/route, and network used). + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here, such as a transaction ID demonstrating the problem. diff --git a/.github/workflows/adoption-snapshot.yml b/.github/workflows/adoption-snapshot.yml new file mode 100644 index 00000000..51bd7d09 --- /dev/null +++ b/.github/workflows/adoption-snapshot.yml @@ -0,0 +1,54 @@ +name: Adoption Snapshot + +on: + schedule: + # Daily at 06:00 UTC — captures GitHub traffic before the 14-day window rolls off + - cron: "0 6 * * *" + workflow_dispatch: # allow manual runs + +# The default GITHUB_TOKEN cannot call GET /repos/.../traffic/* (always 403). +# Add repo secret ADOPTION_GITHUB_TOKEN: a PAT with Administration:read +# (fine-grained) or public_repo (classic). Contents:write on GITHUB_TOKEN +# is still what commits the snapshot files. +permissions: + contents: write + +concurrency: + group: adoption-snapshot + cancel-in-progress: false + +jobs: + snapshot: + runs-on: ubuntu-latest + steps: + # Pin checkout: this job has contents: write. + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + fetch-depth: 0 + + - name: Run adoption snapshot + env: + GITHUB_TOKEN: ${{ secrets.ADOPTION_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + run: python3 scripts/adoption_snapshot.py --out metrics/ --strict + + - name: Commit snapshot + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add metrics/ + if git diff --staged --quiet; then + echo "No changes to commit (snapshot already up to date)" + exit 0 + fi + git commit -m "chore(metrics): daily adoption snapshot $(date -u +%Y-%m-%d)" + branch="${GITHUB_REF_NAME}" + for attempt in 1 2 3; do + if git pull --rebase origin "$branch" && git push origin "HEAD:$branch"; then + exit 0 + fi + echo "push attempt ${attempt} failed; retrying" + sleep $((attempt * 5)) + done + echo "Failed to push snapshot after 3 rebase retries" + exit 1 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..0259c5df --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,39 @@ +name: Docs + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Install dependencies + run: | + python -m pip install --upgrade pip poetry + poetry install + - name: Docs + run: | + poetry run make docs + - name: Upload build + uses: actions/upload-artifact@v4 + with: + name: build + path: docs/_build/html + - name: add CNAME file + run: | + echo 'uniswap-python.com' > docs/_build/html/CNAME + - name: Deploy + uses: peaceiris/actions-gh-pages@v4 + if: github.ref == 'refs/heads/master' + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/_build/html diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..2047980c --- /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@v5 + 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/.github/workflows/test.yml b/.github/workflows/test.yml index 30ec11bb..2a4dff2f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,38 +1,129 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: test +name: Test on: push: - branches: [ master ] + branches: [ master, dev/v4, dev/v4-fin ] pull_request: - branches: [ master ] + branches: [ master, dev/v4, dev/v4-fin ] + +env: + # The repo secret still points at a rate-limited Infura URL, so CI must use + # the public endpoint directly instead of preferring the secret on push runs. + #PROVIDER_MAINNET: 'https://ethereum.publicnode.com' + PROVIDER_MAINNET: 'https://eth.drpc.org' + PROVIDER_ARBITRUM: 'https://arb1.arbitrum.io/rpc' + PROVIDER_XDAI: 'https://rpc.ankr.com/gnosis' jobs: - build: + test: + name: test (v${{ matrix.uniswap-version }}, ${{ matrix.network }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + uniswap-version: [2, 3, 4] + network: ["mainnet"] + # include: + # - network: arbitrum + # uniswap-version: 3 + #include: + # - network: xdai + # uniswap-version: 3 + #include: + # - network: optimism + # uniswap-version: 3 + #include: + # - network: polygon + # uniswap-version: 3 steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v1 + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: 3.8 - - name: Set up Node - uses: actions/setup-node@v2-beta + python-version: '3.12' + # Set up poetry cache, from https://github.com/python-poetry/poetry/blob/45a9b8f20384591d0a33ae876bcf23656f928ec0/.github/workflows/main.yml + - name: Get full python version + id: full-python-version + run: | + version=$(python3 -c "import sys; print('-'.join(str(v) for v in sys.version_info[:3]))") + echo "version=$version" >> "$GITHUB_OUTPUT" + - name: Set up poetry + run: | + python -m pip install --upgrade pip poetry + poetry config virtualenvs.in-project true + + - name: Set up cache + uses: actions/cache@v4 + id: cache with: - node-version: '12' + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Ensure cache is healthy + if: steps.cache.outputs.cache-hit == 'true' + run: timeout 10s poetry run pip --version || rm -rf .venv + - name: Install dependencies run: | - python -m pip install --upgrade pip poetry poetry install - npm install -g ganache-cli - - name: Typecheck - run: | - make typecheck + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + - name: Test env: - MAINNET_PROVIDER: ${{ secrets.MAINNET_PROVIDER }} + # Use the secret if available, otherwise fallback to the public key + PROVIDER: ${{ ((matrix.network == 'mainnet') && env.PROVIDER_MAINNET) || ((matrix.network == 'arbitrum') && env.PROVIDER_ARBITRUM) }} + UNISWAP_VERSION: ${{ matrix.uniswap-version }} run: | make test + + - name: "Upload coverage to Codecov" + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: true + + typecheck: + name: typecheck + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + # Set up poetry cache, from https://github.com/python-poetry/poetry/blob/45a9b8f20384591d0a33ae876bcf23656f928ec0/.github/workflows/main.yml + - name: Get full python version + id: full-python-version + run: | + version=$(python3 -c "import sys; print('-'.join(str(v) for v in sys.version_info[:3]))") + echo "version=$version" >> "$GITHUB_OUTPUT" + + - name: Set up poetry + run: | + python -m pip install --upgrade pip poetry + poetry config virtualenvs.in-project true + + - name: Set up cache + uses: actions/cache@v4 + id: cache + with: + path: .venv + key: venv-${{ runner.os }}-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Ensure cache is healthy + if: steps.cache.outputs.cache-hit == 'true' + run: timeout 10s poetry run pip --version || rm -rf .venv + + - name: Install dependencies + run: | + python -m pip install --upgrade pip poetry + poetry install + + - name: Typecheck + run: | + make typecheck diff --git a/.gitignore b/.gitignore index 71a12ed6..a3c4235c 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ wheels/ .installed.cfg *.egg .idea/ +.vscode/ __pycache__/ # PyInstaller diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..99b348fa --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.15.2 + hooks: + # Run the linter. + - id: ruff-check + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index c3bee52f..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -recursive-include assets/* \ No newline at end of file diff --git a/Makefile b/Makefile index ec622ec0..ec5ec57e 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,24 @@ -.PHONY: test typecheck lint precommit +.PHONY: test typecheck lint precommit docs test: - poetry run pytest -v --cov=uniswap --cov-report html + poetry run pytest -v --tb=auto --maxfail=20 --cov=uniswap --cov-report html --cov-report term --cov-report xml typecheck: poetry run mypy --pretty lint: - poetry run flake8 + poetry run ruff check uniswap + +format: + poetry run ruff format uniswap + +format-abis: + npx prettier --write --parser=json uniswap/assets/*/*.abi precommit: make typecheck make lint make test + +docs: + cd docs/ && make html diff --git a/README.md b/README.md index d5e89532..cdf3dedb 100644 --- a/README.md +++ b/README.md @@ -1,192 +1,150 @@

- +

# uniswap-python -[![GitHub Actions](https://github.com/shanefontaine/uniswap-python/workflows/test/badge.svg)](https://github.com/shanefontaine/uniswap-python/actions) +[![GitHub Actions](https://github.com/shanefontaine/uniswap-python/workflows/Test/badge.svg)](https://github.com/shanefontaine/uniswap-python/actions) +[![codecov](https://codecov.io/gh/uniswap-python/uniswap-python/branch/master/graph/badge.svg?token=VHAZHHLFX8)](https://codecov.io/gh/uniswap-python/uniswap-python) [![Downloads](https://pepy.tech/badge/uniswap-python)](https://pepy.tech/project/uniswap-python) [![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/shanefontaine/uniswap-python/master/LICENSE) +[![PyPI](https://img.shields.io/pypi/v/uniswap-python)](https://pypi.org/project/uniswap-python/) [![Typechecking: Mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) +[![GitPOAP Badge](https://public-api.gitpoap.io/v1/repo/uniswap-python/uniswap-python/badge)](https://www.gitpoap.io/gh/uniswap-python/uniswap-python) -The unofficial Python client for [Uniswap](https://uniswap.io/). +[![GitHub Repo stars](https://img.shields.io/github/stars/uniswap-python/uniswap-python?style=social)](https://github.com/uniswap-python/uniswap-python/stargazers) +[![Twitter Follow](https://img.shields.io/twitter/follow/UniswapPython?label=Follow&style=social)](https://twitter.com/UniswapPython) -_The authors are in no way affiliated with or funded by Uniswap, uniswap.io, or any subsidiaries or affiliates of any of the previously mentioned entities._ +The unofficial Python client for [Uniswap](https://uniswap.io/). -### This package supports Uniswap V2 as of version 0.4.0. +Documentation is available at https://uniswap-python.com/ ## Functionality + * A simple to use Python wrapper for all available contract functions and variables +* A basic CLI to get prices and token metadata * Simple parsing of data returned from the Uniswap contract -## Under Development -* Better error handling +### Supports + + - Uniswap v4 (as of v0.8.0, beta) + - Swaps, price quoting, liquidity management + - Pool discovery via pool cache service + - Uniswap v3 (as of v0.5.0) + - Including beta support for Arbitrum & Optimism deployments (as of v0.5.4) + - Uniswap v2 (as of v0.4.0) + - Uniswap v1 (deprecated) + - Various forks (untested, but should work) + - Honeyswap (xDai) + - Pancakeswap (BSC) + - Sushiswap (mainnet) ## Getting Started -This README is documentation on the syntax of the python client presented in this repository. See function docstrings for full syntax details. -This API attempts to present a clean interface to Uniswap, but in order to use it to its full potential, you must familiarize yourself with the official Uniswap documentation. -* https://docs.uniswap.io/ +See our [Getting started guide](https://uniswap-python.com/getting-started.html) in the documentation. -You may manually install the project or use pip: +## Testing -```python -pip install uniswap-python +Unit tests are under development using the pytest framework. Contributions are welcome! -# or +Tests run on a fork of mainnet using [Anvil](https://getfoundry.sh) (part of Foundry). Install Foundry with: -pip install git+git://github.com/shanefontaine/uniswap-python.git +```sh +curl -L https://foundry.paradigm.xyz | bash +export PATH="$PATH:$HOME/.foundry/bin" +foundryup ``` -### Environment Variables -The program expects an environment variables to be set in order to run the program. You can use an Infura node, since the transactions are being signed locally and broadcast as a raw transaction. The environment variable is: +To run the full test suite, in the project directory set the `PROVIDER` env variable to a mainnet provider, and run: -``` -PROVIDER # HTTP Provider for web3 +```sh +poetry install +export PROVIDER= # URL of provider, e.g. https://mainnet.infura.io/v3/... +make test +# or... +poetry run pytest --capture=no # doesn't capture output (verbose) ``` -### Public Client -Only some endpoints in the API are available to everyone. The public endpoints can be reached using PublicClient - -```python -from uniswap import Uniswap -address = "YOUR ADDRESS" # or "0x0000000000000000000000000000000000000000", if you're not making transactions -private_key = "YOUR PRIVATE KEY" # or None, if you're not going to make transactions -uniswap_wrapper = Uniswap(address, private_key, version=2) # pass version=2 to use Uniswap v2 -eth = "0x0000000000000000000000000000000000000000" -bat = "0x0D8775F648430679A709E98d2b0Cb6250d2887EF" -dai = "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359" -``` +## Funding -#### Market Methods -* [get_fee_maker](https://docs.uniswap.io/) -```python -uniswap_wrapper.get_fee_maker() -``` +Development of this library has been funded by: -* [get_fee_taker](https://docs.uniswap.io/) -```python -uniswap_wrapper.get_fee_taker() -``` +* The **[Uniswap Grants Program](https://www.uniswapfoundation.org/grants)**, which awarded a $15k grant (2021) that funded the development of Uniswap v3 support. +* **Community donations** through [Gitcoin Grants](https://gitcoin.co/grants/2631/uniswap-python), which were used to compensate contributors (see [#181](https://github.com/uniswap-python/uniswap-python/discussions/181)). +* **[Superuser Labs](https://superuserlabs.org/)**, Erik's company, which has sponsored continued development and maintenance, including funding [@liquid-8](https://github.com/liquid-8)'s work on Uniswap v4 support. -* [get_eth_token_input_price](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L416) -```python -# Get the public price for ETH to Token trades with an exact input. -uniswap_wrapper.get_eth_token_input_price(bat, 1*10**18) -uniswap_wrapper.get_eth_token_input_price(dai, 5*10**18) -``` +## Authors -* [get_token_eth_input_price](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L437) -```python -# Get the public price for token to ETH trades with an exact input. -uniswap_wrapper.get_token_eth_input_price(bat, 1*10**18) -uniswap_wrapper.get_token_eth_input_price(dai, 5*10**18) -``` +* [Shane Fontaine](https://twitter.com/shanecoin) +* [Erik Bjäreholt](https://twitter.com/ErikBjare) +* [@liquid-8](https://github.com/liquid-8) +* ...and [others](https://github.com/uniswap-python/uniswap-python/graphs/contributors) -* [get_eth_token_output_price](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L426) -```python -# Get the public price for ETH to Token trades with an exact output -uniswap_wrapper.get_eth_token_output_price(bat, 1*10**18) -uniswap_wrapper.get_eth_token_output_price(dai, 5*10**18) -``` +Contributors also earn this beautiful [GitPOAP](https://gitpoap.notion.site/What-s-a-GitPOAP-5b085daac4b4429994b5231be028b3d9) for their contributions! -* [get_token_eth_output_price](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L448) -```python -# Get the public price for token to ETH trades with an exact output. -uniswap_wrapper.get_token_eth_output_price(bat, 1*10**18) -uniswap_wrapper.get_token_eth_output_price(dai, 5*10**18) -``` + + + -#### ERC20 Pool Methods (v1 only) -* [get_ex_eth_balance](https://docs.uniswap.io/smart-contract-integration/vyper) -```python -# Get the balance of ETH in an exchange contract. -uniswap_wrapper.get_ex_eth_balance(bat) -``` +## Changelog -* [get_ex_token_balance](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L469) -```python -# Get the balance of a token in an exchange contract. -uniswap_wrapper.get_ex_token_balance(bat) -``` +_0.9.0_ +* Added: Async versions of Uniswap V2-V4 +* Added: V2 and V3 custom routes (untested) +* Added: Async pool cache service for Uniswap V4 +* Removed: V1 support -* [get_exchange_rate](https://github.com/Uniswap/uniswap-frontend/blob/master/src/pages/Pool/AddLiquidity.js#L351) -```python -# Get the exchange rate of token/ETH -uniswap_wrapper.get_exchange_rate(bat) -``` +_0.8.0_ -#### Liquidity Methods (v1 only) +* Added: Uniswap V4 support +* Added: Pool cache service for Uniswap V4 +* Full parameters customization for V4 transactions; V4 transaction replacement and cancelling. -* [add_liquidity](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L48) -```python -# Add liquidity to the pool. -uniswap_wrapper.add_liquidity(bat, 1*10**18) -``` +_0.7.2_ -* [remove_liquidity](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L83) -```python -# Remove liquidity from the pool. -uniswap_wrapper.remove_liquidity(bat, 1*10**18) -``` +* 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) -#### Trading -* make_trade - * eth_to_token_input - * [ethToTokenSwapInput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L127) - * [ethToTokenTransferInput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L162) - * token_to_eth_input - * [tokenToEthSwapInput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L202) - * [tokenToEthTransferInput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L232) - * token_to_token_input - * [tokenToTokenSwapInput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L271) - * [tokenToTokenTransferInput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L307) -```python -# Make a trade based on the input parameters -uniswap_wrapper.make_trade(eth, bat, 1*10**18) # calls _eth_to_token_input -uniswap_wrapper.make_trade(bat, eth, 1*10**18) # calls _token_to_eth_input -uniswap_wrapper.make_trade(bat, dai, 1*10**18) # calls _token_to_token_input -uniswap_wrapper.make_trade(eth, bat, 1*10**18, "0x123...") # calls _eth_to_token_input -``` +_0.7.1_ -* make_trade_output - * eth_to_token_swap_output - * [ethToTokenSwapOutput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L167) - * [ethToTokenTransferOutput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L197) - * token_to_eth_swap_output - * [tokenToEthSwapOutput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L237) - * [tokenToEthTransferOutput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L266) - * token_to_token_swap_output - * [tokenToTokenSwapOutput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L312) - * [tokenToTokenTransferOutput](https://github.com/Uniswap/contracts-vyper/blob/master/contracts/uniswap_exchange.vy#L349)) -```python -# Make a trade where the output qty is known based on the input parameters -uniswap_wrapper.make_trade_output(eth, bat, 1*10**18) # calls _eth_to_token_swap_output -uniswap_wrapper.make_trade_output(bat, eth, 1*10**18) # calls _token_to_eth_swap_output -uniswap_wrapper.make_trade_output(bat, dai, 1*10**18, "0x123...") # calls _token_to_token_swap_output -``` +* incomplete changelog -## Testing -Unit tests are under development using the pytest framework. Contributions are welcome! +_0.7.0_ -Test are run on a fork of the main net using ganache-cli. You need to install it with `npm install -g ganache-cli` before running tests. +* incomplete changelog -To run the full test suite, in the project directory run: +_0.5.4_ -``` -make test -``` +* added use of gas estimation instead of a fixed gas limit (to support Arbitrum) +* added `use_estimate_gas` constructor argument (used in testing) +* added constants/basic support for Arbitrum, Optimism, Polygon, and Fantom. (untested) +* incomplete changelog -## Authors -* [Shane Fontaine](https://twitter.com/shanecoin) -* [Erik Bjäreholt](https://twitter.com/ErikBjare) +_0.5.3_ -## Ownership Disclosure +* incomplete changelog -* I own some BAT and DAI tokens that are seen in the examples above. These tokens are used only for example purposes and are not meant to be an endorsement. I am not affiliated with BAT, Brave, Basic Attention Token, Brave Browser, DAI, Maker, MakerDAO, or any subsidiaries. +_0.5.2_ -## Changelog +* incomplete changelog + +_0.5.1_ + +* Updated dependencies +* Fixed minor typing issues + +_0.5.0_ + +* Basic support for Uniswap V3 +* Added new methods `get_price_input` and `get_price_output` +* Made a lot of previously public methods private +* Added documentation site +* Removed ENS support (which was probably broken to begin with) + +_0.4.6_ + +* Bug fix: Update bleach package from 3.1.4 to 3.3.0 _0.4.5_ * Bug fix: Use .eth instead of .ens @@ -218,7 +176,7 @@ _A huge thank you [Erik Bjäreholt](https://github.com/ErikBjare) for adding Uni * Switched from setup.py to pyproject.toml/poetry * Switched from Travis to GitHub Actions * For CI to work in your repo, you need to set the secret MAINNET_PROVIDER. I use Infura. -* Running tests on a local fork of mainnet using ganache-cli (started as a fixture) +* Running tests on a local fork of mainnet using Anvil/Foundry (started as a fixture) * Fixed tests for make_trade and make_trade_output * Added type annotations to the entire codebase and check them with mypy in CI * Formatted entire codebase with black diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..35cde5cd --- /dev/null +++ b/codecov.yml @@ -0,0 +1,4 @@ +coverage: + status: + project: off + patch: off diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d4bb2cbb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_static/favicon.png b/docs/_static/favicon.png new file mode 100644 index 00000000..f7e028b7 Binary files /dev/null and b/docs/_static/favicon.png differ diff --git a/docs/_static/logo.png b/docs/_static/logo.png new file mode 100644 index 00000000..9d63e70f Binary files /dev/null and b/docs/_static/logo.png differ diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..69171447 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,49 @@ +API Reference +============= + +.. automodule:: uniswap + +Uniswap class +------------- + +.. autoclass:: Uniswap + :members: + + + + +Uniswap4 class +-------------- +.. automodule:: uniswap.uniswap4 + +.. autoclass:: Uniswap4 + :members: + + +Pools cache class +----------------- +.. automodule:: uniswap.util + +.. autoclass:: V4pools + :members: + +Token class +----------- + +.. automodule:: uniswap.token + +.. autoclass:: BaseToken + :members: + +.. autoclass:: ERC20Token + :inherited-members: + :members: + +Exceptions +---------- + +.. automodule:: uniswap.exceptions + +.. autoexception:: InvalidToken + +.. autoexception:: InsufficientBalance diff --git a/docs/cli.rst b/docs/cli.rst new file mode 100644 index 00000000..11ab6795 --- /dev/null +++ b/docs/cli.rst @@ -0,0 +1,42 @@ +Command line interface +====================== + +uniswap-python provides a basic command line interface named ``unipy``, to let you easier query the chain for things like the current price and token metadata. + +Example usage +------------- + +**Note:** uniswap-python contains a small database of token contract addresses for convenience. You can always provide a contract address in place of a shorthand, and need to do so for all tokens not in the bundled database, or if you're not on mainnet. + +.. code:: shell + + # Get price for 1 WETH quoted in DAI + $ unipy price WETH DAI + 3350.883387688622 + + # Get price for 1 WETH quoted in DAI, skip decimal normalization + $ unipy price --raw WETH DAI + 3350883387688622003541 + + # Get price for 1 WETH quoted in USDT + $ unipy price WETH 0xdac17f958d2ee523a2206206994597c13d831ec7 + 3348.128969 + + # Get token metadata from its ERC20 contract + $ unipy token 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 + {'name': 'Wrapped Ether', 'symbol': 'WETH', 'decimals': 18} + + # List known/hardcoded tokens, with metadata + $ unipy tokendb --metadata + {'name': 'Wrapped Ether', 'symbol': 'WETH', 'decimals': 18} + {'name': 'Dai Stablecoin', 'symbol': 'DAI', 'decimals': 18} + {'name': 'Wrapped BTC', 'symbol': 'WBTC', 'decimals': 8} + ... + + +Usage +----- + +.. click:: uniswap.cli:main + :prog: unipy + :nested: full diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..a831965b --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,84 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +sys.path.insert(0, os.path.abspath("..")) + + +# -- Project information ----------------------------------------------------- + +project = "uniswap-python" +author = "Shane Fontaine, Erik Bjäreholt, and contributors" +copyright = "2021, " + author + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.extlinks", + "sphinx.ext.autosectionlabel", + "sphinx_click", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +extlinks = { + "issue": ("https://github.com/shanefontaine/uniswap-python/issues/%s", "issue #%s"), +} + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_book_theme" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +html_title = "uniswap-python" +html_logo = "_static/logo.png" +html_favicon = "_static/favicon.png" + +html_theme_options = { + "repository_url": "https://github.com/shanefontaine/uniswap-python", + "path_to_docs": "docs", + "use_repository_button": True, + "use_edit_page_button": True, + "extra_navbar": """ +

+ Back to GitHub +

""", +} + +show_navbar_depth = 2 + + +# Autodoc config + +autoclass_content = "both" +autodoc_member_order = "bysource" diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 00000000..021ff0ce --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,196 @@ +Examples +======== + +This page shows common usage patterns for Uniswap v2 and v3. For v4 examples, +see the dedicated :doc:`v4` guide. + +The code snippets here mirror the `test suite +`_, which +runs every example against a live mainnet fork using +`Anvil `_. + +.. contents:: Table of contents + :local: + :depth: 2 + +Uniswap v2 +---------- + +Initialization +`````````````` + +.. code:: python + + from uniswap import Uniswap + + ETH = "0x0000000000000000000000000000000000000000" + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" + + uni = Uniswap( + address="0xYOUR_ADDRESS", + private_key="0xYOUR_PRIVATE_KEY", # or None for read-only + version=2, + provider="https://mainnet.infura.io/v3/YOUR_PROJECT_ID", + ) + +Getting prices +`````````````` + +.. code:: python + + ONE_ETH = 10**18 + + # How much USDC do I get for 1 ETH? + usdc_out = uni.get_price_input(ETH, USDC, ONE_ETH) + print(f"1 ETH → {usdc_out / 10**6:.2f} USDC") + + # How much ETH do I need to buy exactly 1000 USDC? + eth_needed = uni.get_price_output(ETH, USDC, 1000 * 10**6) + print(f"ETH needed for 1000 USDC: {eth_needed / ONE_ETH:.4f}") + +Making swaps +```````````` + +.. code:: python + + # Sell 0.1 ETH, receive USDC (exact input) + tx = uni.make_trade(ETH, USDC, ONE_ETH // 10) + + # Buy exactly 100 USDC, pay in ETH (exact output) + tx = uni.make_trade_output(ETH, USDC, 100 * 10**6) + + # Sell ETH → DAI with a custom recipient + tx = uni.make_trade(ETH, DAI, ONE_ETH // 10, recipient="0xSOME_OTHER_ADDRESS") + +Multi-hop swaps +``````````````` + +For pairs without a direct v2 pool, route through an intermediate token: + +.. code:: python + + WBTC = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + + # ETH → WBTC (routed automatically through WETH by the v2 router) + wbtc_out = uni.get_price_input(ETH, WBTC, ONE_ETH // 10) + tx = uni.make_trade(ETH, WBTC, ONE_ETH // 10) + +Uniswap v3 +---------- + +v3 adds concentrated liquidity pools at multiple fee tiers. Always specify +``fee`` to select the pool — the right tier depends on the pair's volatility. + +Initialization +`````````````` + +.. code:: python + + from uniswap import Uniswap + + ETH = "0x0000000000000000000000000000000000000000" + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + WBTC = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" + DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" + + uni = Uniswap( + address="0xYOUR_ADDRESS", + private_key="0xYOUR_PRIVATE_KEY", + version=3, + provider="https://mainnet.infura.io/v3/YOUR_PROJECT_ID", + ) + +Fee tiers +````````` + +Common v3 fee tiers: + +- ``500`` — 0.05% (stablecoin pairs, e.g. USDC/USDT) +- ``3000`` — 0.30% (most pairs, e.g. ETH/USDC) +- ``10000`` — 1.00% (exotic/volatile pairs) + +Getting prices +`````````````` + +.. code:: python + + ONE_ETH = 10**18 + + # Quote using the 0.30% ETH/USDC pool + usdc_out = uni.get_price_input(ETH, USDC, ONE_ETH, fee=3000) + print(f"1 ETH → {usdc_out / 10**6:.2f} USDC (0.30% pool)") + + # Compare with the 0.05% pool (better rate for large trades) + usdc_out_low = uni.get_price_input(ETH, USDC, ONE_ETH, fee=500) + print(f"1 ETH → {usdc_out_low / 10**6:.2f} USDC (0.05% pool)") + + # Exact output quote + eth_needed = uni.get_price_output(ETH, USDC, 1000 * 10**6, fee=500) + +Making swaps +```````````` + +.. code:: python + + # Sell 0.1 ETH for USDC via the 0.05% pool + tx = uni.make_trade(ETH, USDC, ONE_ETH // 10, fee=500) + + # Buy exactly 100 USDC, paying in ETH + tx = uni.make_trade_output(ETH, USDC, 100 * 10**6, fee=500) + +Multi-hop swaps +``````````````` + +The v3 client does not expose a multi-hop path parameter in ``make_trade``. +For pairs without a direct pool, execute two single-hop trades in sequence. +Wait for the first transaction and use the wallet's confirmed balance increase, +not its quote, as the second hop's input: + +.. code:: python + + DAI = "0x6B175474E89094C44Da98b954EedeAC495271d0F" + + # ETH → USDC (first hop, 0.05% pool) + usdc_before = uni.get_token_balance(USDC) + tx1 = uni.make_trade(ETH, USDC, ONE_ETH // 10, fee=500) + uni.w3.eth.wait_for_transaction_receipt(tx1) + usdc_received = uni.get_token_balance(USDC) - usdc_before + + # USDC → DAI (second hop, 0.01% stable pool) + tx2 = uni.make_trade(USDC, DAI, usdc_received, fee=100) + +Liquidity management (v3) +````````````````````````` + +.. code:: python + + from uniswap.util import default_tick_range + + ETH = "0x0000000000000000000000000000000000000000" + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + ONE_ETH = 10**18 + + # Get the pool contract instance + pool = uni.get_pool_instance(ETH, USDC, fee=500) + + # Sensible full-range tick bounds for this fee tier + tick_lower, tick_upper = default_tick_range(fee=500) + + # Mint a liquidity position (returns TxReceipt) + receipt = uni.mint_liquidity( + pool, + amount0=ONE_ETH // 10, + amount1=340 * 10**6, + tick_lower=tick_lower, + tick_upper=tick_upper, + deadline=2**64, + ) + assert receipt["status"] + + # Get your token IDs (ERC-721 NFTs representing positions) + positions = uni.get_liquidity_positions() + token_id = positions[0] + + # Close the position (collects fees + withdraws liquidity in one call) + receipt = uni.close_position(token_id, deadline=2**64) diff --git a/docs/getting-started.rst b/docs/getting-started.rst new file mode 100644 index 00000000..d00dd604 --- /dev/null +++ b/docs/getting-started.rst @@ -0,0 +1,178 @@ +Getting started +=============== + +This library attempts to present a clean interface to Uniswap, but in order to use it to its full potential, you must familiarize yourself with the official Uniswap documentation: + +- V1: https://uniswap.org/docs/v1/ +- V2: https://uniswap.org/docs/v2/ +- V3 and V4: https://docs.uniswap.org/ + +.. contents:: Table of contents + :local: + :depth: 3 + +Installation +------------ + +You can install the latest release from PyPI, or install the latest commit directly from git: + +.. code:: sh + + # Install the latest release from PyPI: + + pip install uniswap-python + + # or install from git: + + pip install git+git://github.com/uniswap-python/uniswap-python.git + + # or clone and install with poetry: + + git clone https://github.com/uniswap-python/uniswap-python.git + cd uniswap-python + poetry install + + +Initializing the Uniswap class +------------------------------ + +If you want to trade you need to provide your address and private key. If not, you can set them to ``None``. + +In addition, the :class:`~uniswap.Uniswap` class takes several optional parameters, as documented in the `API Reference`. + +.. code:: python + + from uniswap import Uniswap + + address = "YOUR ADDRESS" # or None if you're not going to make transactions + private_key = "YOUR PRIVATE KEY" # or None if you're not going to make transactions + version = 2 # specify which version of Uniswap to use + provider = "WEB3 PROVIDER URL" # can also be set through the environment variable `PROVIDER` + uniswap = Uniswap(address=address, private_key=private_key, version=version, provider=provider) + + # Some token addresses we'll be using later in this guide + eth = "0x0000000000000000000000000000000000000000" + bat = "0x0D8775F648430679A709E98d2b0Cb6250d2887EF" + dai = "0x6B175474E89094C44Da98b954EedeAC495271d0F" + + +Environment Variables +````````````````````` + +The program expects an environment variables to be set in order to run the program. You can use an Infura node, since the transactions are being signed locally and broadcast as a raw transaction. The environment variable is: + +.. code:: sh + + PROVIDER # HTTP Provider for web3 + +Gas pricing +``````````` + +To modify the gas pricing strategy you need to pass a custom `Web3` instance to the :class:`~uniswap.Uniswap` constructor. You can find details for how to configure Web3 gas strategies in their `documentation `_. + + +Quoting prices +-------------- + +.. note:: + + These methods assume a certain route for the swap to take, which may not be the optimal route. See :issue:`93` for details. + +There are two functions to retrieve the price for a given pair, one for specifying how much you get given a certain amount of the input token, and another for specifying how much you need to pay to receive a certain amount of the output token. + +:func:`~uniswap.Uniswap.get_price_input` +```````````````````````````````````````` + +Returns the amount of output tokens you get for a given amount of input tokens. + +.. code:: python + + # Returns the amount of DAI you get for 1 ETH (10^18 wei) + uniswap.get_price_input(eth, dai, 10**18) + +:func:`~uniswap.Uniswap.get_price_output` +````````````````````````````````````````` + +Returns the amount of input token you need for the given amount of output tokens. + +.. code:: python + + # Returns the amount of ETH you need to pay (in wei) to get 1000 DAI + uniswap.get_price_output(eth, dai, 1_000 * 10**18) + + +.. note:: + + These methods return the price as an integer in the smallest unit of the token. You need to ensure that you know how many decimals the token you're trying to trade uses to get prices in the common decimal format. See :issue:`12` for details. + + Decimals for common tokens: + + - ETH, DAI, and BAT uses 18 decimals (as you can see in code below) + - WBTC uses 8 decimals + - USDC and USDT uses 6 decimals + + You can look up the number of decimals used by a particular token by looking up the contract on Etherscan. + +Making trades +------------- + +.. note:: + + The same route assumptions and need for handling decimals apply here as those mentioned in the previous section. + +.. warning:: + + Always check the expected price before executing a trade. It's important that you're using a pool with adequate liquidity, or else you may suffer significant losses! (see :issue:`198` and :issue:`208`) + + Use the Uniswap version with the most liquidity for your route, and if using v3, make sure you set the ``fee`` parameter to use the best pool. + +:func:`~uniswap.Uniswap.make_trade` +``````````````````````````````````` + +.. code:: python + + # Make a trade by specifying the quantity of the input token you wish to sell + uniswap.make_trade(eth, bat, 1*10**18) # sell 1 ETH for BAT + uniswap.make_trade(bat, eth, 1*10**18) # sell 1 BAT for ETH + uniswap.make_trade(bat, dai, 1*10**18) # sell 1 BAT for DAI + uniswap.make_trade(eth, bat, 1*10**18, "0x123...") # sell 1 ETH for BAT, and send the BAT to the provided address + uniswap.make_trade(dai, usdc, 1*10**18, fee=500) # sell 1 DAI for USDC using the 0.05% fee pool (v3 only) + +:func:`~uniswap.Uniswap.make_trade_output` +`````````````````````````````````````````` + +.. code:: python + + # Make a trade by specifying the quantity of the output token you wish to buy + uniswap.make_trade_output(eth, bat, 1*10**18) # buy ETH for 1 BAT + uniswap.make_trade_output(bat, eth, 1*10**18) # buy BAT for 1 ETH + uniswap.make_trade_output(bat, dai, 1*10**18, "0x123...") # buy BAT for 1 DAI, and send the BAT to the provided address + uniswap.make_trade_output(dai, usdc, 1*10**8, fee=500) # buy USDC for 1 DAI using the 0.05% fee pool (v3 only) + + +Pool Methods (v1 only) +--------------------------- + +.. code:: python + + # Get the balance of ETH in an exchange contract. + uniswap.get_ex_eth_balance(bat) + + # Get the balance of a token in an exchange contract. + uniswap.get_ex_token_balance(bat) + + # Get the exchange rate of token/ETH + uniswap.get_exchange_rate(bat) + + +Liquidity Methods (v1 only) +--------------------------- + +.. code:: python + + # Add liquidity to the pool. + uniswap.add_liquidity(bat, 1*10**18) + + # Remove liquidity from the pool. + uniswap.remove_liquidity(bat, 1*10**18) + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..76dfb2fe --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,43 @@ +.. image:: _static/logo.png + :width: 200 + :alt: uniswap-python logo + :align: center + +| +| + +Welcome to uniswap-python's documentation! +========================================== + +.. image:: https://img.shields.io/github/stars/uniswap-python/uniswap-python?style=social + :target: https://github.com/uniswap-python/uniswap-python + :alt: GitHub Repo stars +.. image:: https://img.shields.io/twitter/follow/UniswapPython?label=Follow&style=social + :target: https://twitter.com/UniswapPython + :alt: Twitter follow + + +.. note:: We're in the process of improving the documentation of the project. You will find some docs here, but there's also some documentation in the `README `_ that you might want to look at. + +This library lets you easily retrieve prices and make trades on all Uniswap versions. + +A good place to start is the :doc:`getting-started` guide. + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + getting-started + v4 + api + cli + examples + supported-deployments + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..922152e9 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/supported-deployments.rst b/docs/supported-deployments.rst new file mode 100644 index 00000000..694bc888 --- /dev/null +++ b/docs/supported-deployments.rst @@ -0,0 +1,17 @@ +Supported deployments +===================== + +uniswap-python supports the standard deployments of Uniswap v1, v2, and v3, to both mainnet and testnets (and in the future hopefully L2's like Optimism and Arbitrum). + + +Using with forks/other chains +----------------------------- + +While not officially supported nor tested, you can use this library to trade across several Uniswap forks, like: + +- Sushiswap (mainnet) +- Honeyswap (xDai) +- Pancakeswap (BSC) +- And any other forks that don't deviate too much from the ABI. + +See the issues for details about how to do so. diff --git a/docs/v4.rst b/docs/v4.rst new file mode 100644 index 00000000..25937594 --- /dev/null +++ b/docs/v4.rst @@ -0,0 +1,409 @@ +Uniswap v4 +========== + +Uniswap v4 introduces a fundamentally different architecture from v2 and v3. +All pools live inside a single **PoolManager** singleton; positions are ERC-721 +tokens managed by the **PositionManager**; and every pool is uniquely identified +by a :class:`~uniswap.types.PoolKey` struct instead of a standalone contract address. + +.. contents:: Table of contents + :local: + :depth: 2 + +The PoolKey +----------- + +A :class:`~uniswap.types.PoolKey` is a five-field tuple that uniquely identifies a pool: + +.. code:: python + + from uniswap.types import PoolKey + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + eth_usdc_pool = PoolKey( + currency0=ETH_ADDRESS, # "0x000...000" — ETH is the zero address in v4 + currency1="0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", # USDC + fee=500, # 0.05% fee tier + tick_spacing=10, # tick spacing that matches the fee tier + hooks=ZERO_HOOK, # "0x000...000" means no hook contract + ) + +.. note:: + + In Uniswap v4, native ETH is represented by the **zero address** + (``0x000...000``), not WETH. The two currencies in a pool are always ordered + so that ``currency0 < currency1`` (lexicographic address ordering). + +Common fee tiers and their tick spacings: + ++------------+--------+--------------+ +| ``fee`` | Rate | Tick spacing | ++============+========+==============+ +| ``100`` | 0.01% | 1 | ++------------+--------+--------------+ +| ``500`` | 0.05% | 10 | ++------------+--------+--------------+ +| ``3000`` | 0.30% | 60 | ++------------+--------+--------------+ +| ``10000`` | 1.00% | 200 | ++------------+--------+--------------+ + +Connecting to Uniswap v4 +------------------------ + +Import :class:`~uniswap.Uniswap4` and provide a wallet address, private key, and +a mainnet Web3 provider. Omit ``private_key`` for read-only use. + +.. code:: python + + from uniswap import Uniswap4 + + address = "0xYOUR_ADDRESS" + private_key = "0xYOUR_PRIVATE_KEY" # or None for read-only + + uni = Uniswap4( + address=address, + private_key=private_key, + provider="https://mainnet.infura.io/v3/YOUR_PROJECT_ID", + ) + + # Or pass a Web3 instance directly: + from web3 import Web3 + w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID")) + uni = Uniswap4(address=address, private_key=private_key, web3=w3) + +The ``PROVIDER`` environment variable is also accepted: + +.. code:: sh + + export PROVIDER="https://mainnet.infura.io/v3/YOUR_PROJECT_ID" + +Getting the spot price +---------------------- + +:func:`~uniswap.Uniswap4.get_token_token_spot_price` returns the current price +of ``token0`` denominated in ``token1``, derived from the pool's ``sqrtPriceX96`` +slot via the v4 StateView contract. + +.. code:: python + + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + + # How many USDC per 1 ETH? + price = uni.get_token_token_spot_price( + token0=ETH_ADDRESS, + token1=USDC, + fee=500, + tick_spacing=10, + hooks=ZERO_HOOK, + ) + print(f"ETH/USDC spot price: {price:.2f}") # e.g. 3412.54 + + # Inverse: ETH price in USDC terms + price_inv = uni.get_token_token_spot_price(USDC, ETH_ADDRESS, fee=500, tick_spacing=10) + +Quoting a trade +--------------- + +The quote functions call the on-chain v4 Quoter contract and account for +liquidity depth and fees. Use them instead of the spot price whenever you need +an accurate estimate for a specific trade size. + +Exact input — how much output do I get? +``````````````````````````````````````` + +:func:`~uniswap.Uniswap4.get_price_input` returns the amount of ``token1`` +received for a fixed amount of ``token0``. + +.. code:: python + + from uniswap.constants import ETH_ADDRESS + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + ONE_ETH = 10**18 + + # How much USDC do I get for 1 ETH? + usdc_out = uni.get_price_input( + token0=ETH_ADDRESS, + token1=USDC, + qty=ONE_ETH, + fee=500, + tick_spacing=10, + ) + print(f"1 ETH → {usdc_out / 10**6:.2f} USDC") + +For a 2-hop route (e.g. ETH → USDC → USDT), pass a ``route`` list of +:class:`~uniswap.types.PoolKey` objects instead of ``fee``/``tick_spacing``: + +.. code:: python + + from uniswap.types import PoolKey + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + USDT = "0xdAC17F958D2ee523a2206206994597C13D831ec7" + + eth_usdc = PoolKey(currency0=ETH_ADDRESS, currency1=USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK) + usdc_usdt = PoolKey(currency0=USDC, currency1=USDT, fee=100, tick_spacing=1, hooks=ZERO_HOOK) + + usdt_out = uni.get_price_input( + token0=ETH_ADDRESS, + token1=USDT, + qty=ONE_ETH, + route=[eth_usdc, usdc_usdt], + ) + +Exact output — how much input do I need? +````````````````````````````````````````` + +:func:`~uniswap.Uniswap4.get_price_output` returns the amount of ``token0`` +required to receive a fixed amount of ``token1``. + +.. code:: python + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + + # How much ETH do I need to buy exactly 1000 USDC? + eth_needed = uni.get_price_output( + token0=ETH_ADDRESS, + token1=USDC, + qty=1000 * 10**6, # 1000 USDC in smallest unit + fee=500, + tick_spacing=10, + ) + print(f"ETH needed for 1000 USDC: {eth_needed / 10**18:.4f}") + +Estimating price impact +----------------------- + +:func:`~uniswap.Uniswap4.estimate_price_impact` compares the quoted price to +the pool spot price to estimate slippage. + +.. code:: python + + ONE_ETH = 10**18 + + impact = uni.estimate_price_impact( + token0=ETH_ADDRESS, + token1=USDC, + qty=ONE_ETH, + fee=500, + tick_spacing=10, + ) + print(f"Price impact: {impact:.4%}") # e.g. "Price impact: 0.0152%" + + if impact > 0.01: # warn above 1% + print("High price impact — consider splitting the trade.") + +Making a swap +------------- + +.. warning:: + + Always verify the expected output (via :func:`~uniswap.Uniswap4.get_price_input` + or :func:`~uniswap.Uniswap4.estimate_price_impact`) before executing a swap. + Low-liquidity pools can cause large, unexpected losses. + +:func:`~uniswap.Uniswap4.make_swap_input` executes a swap for a fixed input +amount. Pass the quoted output as ``qtycap``; the client applies its configured +``max_slippage`` tolerance when it builds the transaction. + +.. code:: python + + from uniswap.types import PoolKey + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + ONE_ETH = 10**18 + SELL_AMOUNT = ONE_ETH // 10 # 0.1 ETH + + pool_key = PoolKey( + currency0=ETH_ADDRESS, + currency1=USDC, + fee=500, + tick_spacing=10, + hooks=ZERO_HOOK, + ) + + # Uniswap4 applies its configured max_slippage (1% by default) to this quote + expected_usdc = uni.get_price_input(ETH_ADDRESS, USDC, SELL_AMOUNT, fee=500, tick_spacing=10) + + tx_hash = uni.make_swap_input( + input_token=ETH_ADDRESS, + output_token=USDC, + qty=SELL_AMOUNT, + qtycap=expected_usdc, + swap_pool_key=pool_key, + ) + receipt = uni.w3.eth.wait_for_transaction_receipt(tx_hash) + print(f"Swap mined in block {receipt['blockNumber']}") + +:func:`~uniswap.Uniswap4.make_swap_output` buys an exact output amount. Pass the +quoted input as ``qtycap``; the client applies ``max_slippage`` to derive the +maximum input: + +.. code:: python + + # Buy exactly 100 USDC; max_slippage is applied to this quoted ETH input + expected_eth = uni.get_price_output(ETH_ADDRESS, USDC, 100 * 10**6, fee=500, tick_spacing=10) + + tx_hash = uni.make_swap_output( + input_token=ETH_ADDRESS, + output_token=USDC, + qty=100 * 10**6, + qtycap=expected_eth, + swap_pool_key=pool_key, + ) + +Discovering pools +----------------- + +:class:`~uniswap.util.V4pools` scans ``PoolManager Initialize`` events to find +all pools for a given pair of tokens. + +.. code:: python + + from web3 import Web3 + from uniswap.util import V4pools + from uniswap.constants import ETH_ADDRESS + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + + w3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/YOUR_PROJECT_ID")) + pools = V4pools(w3) + + # Scan the chain (slow on first run — save to disk for reuse) + pools.fetch_poolkey_data(chunk_size=2000) + pools.save_poolkeys_list("/tmp/v4_pools.json") + + # Load from disk on subsequent runs + pools.load_poolkeys_list("/tmp/v4_pools.json") + + # Get all ETH/USDC pools + eth_usdc_pools = pools.get_poolkeys_sublist(ETH_ADDRESS, USDC) + for pk in eth_usdc_pools: + print(f"fee={pk.fee}, tick_spacing={pk.tick_spacing}, hooks={pk.hooks}") + +Managing liquidity +------------------ + +Uniswap v4 positions are ERC-721 tokens managed by the PositionManager. + +Creating a pool +``````````````` + +New pools are created via :func:`~uniswap.Uniswap4.create_pool`, which sets the +initial price. The price is expressed as ``sqrtPriceX96`` (an integer). + +.. code:: python + + import math + from uniswap.types import PoolKey + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + pool_key = PoolKey( + currency0=ETH_ADDRESS, currency1=USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK + ) + + # Encode an initial price of ~3400 USDC per ETH as sqrtPriceX96 + # price_ratio = token1_amount / token0_amount (in smallest units) + price_ratio = (3400 * 10**6) / 10**18 + sqrt_price_x96 = int(math.sqrt(price_ratio) * 2**96) + + tx = uni.create_pool(pool_key=pool_key, sqrt_price_x96=sqrt_price_x96) + uni.w3.eth.wait_for_transaction_receipt(tx) + +Minting a position +`````````````````` + +:func:`~uniswap.Uniswap4.mint_position` adds liquidity to a tick range and +returns an ERC-721 NFT representing the position. + +.. code:: python + + ONE_ETH = 10**18 + + tx = uni.mint_position( + pool_key=pool_key, + tick_lower=-887270, # near minimum tick + tick_upper=887270, # near maximum tick (full range) + liquidity=10**18, # amount of liquidity to provide + amount0=ONE_ETH // 10, # max ETH to spend + amount1=340 * 10**6, # max USDC to spend + ) + + # Retrieve the ERC-721 token ID(s) minted by this transaction + token_ids = uni.get_minted_token_id(tx.hex()) + token_id = token_ids[0] + print(f"Minted position with token ID: {token_id}") + +Reading position state +`````````````````````` + +.. code:: python + + from uniswap.types import PoolKey + + position = uni.get_position_info(token_id) + liquidity = uni.position_manager_get_position_liquidity(token_id) + print(f"Liquidity: {liquidity}") + + # Read the position value (amounts of token0/token1 at current price) + ETH_DECIMALS = 18 + USDC_DECIMALS = 6 + value = uni.get_position_value(token_id, ETH_DECIMALS, USDC_DECIMALS) + print(f"token0 amount: {value['amount0']}, token1 amount: {value['amount1']}") + +Collecting fees and removing liquidity +``````````````````````````````````````` + +.. code:: python + + from uniswap.types import PoolKey + + # Build the PoolKey from position info + position = uni.get_position_info(token_id) + pool_key = PoolKey( + position["currency0"], position["currency1"], + position["fee"], position["tickSpacing"], position["hooks"] + ) + + # Collect all accumulated fees + uni.collect_fees(pool_key=pool_key, token_id=token_id) + + # Remove all liquidity (burn requires the position to be fully empty) + liquidity = uni.position_manager_get_position_liquidity(token_id) + uni.decrease_liquidity( + pool_key=pool_key, + token_id=token_id, + liquidity=liquidity, + amount0_min=0, + amount1_min=0, + ) + + # Burn the position NFT once fully emptied + uni.burn_position(pool_key=pool_key, token_id=token_id, amount0_min=0, amount1_min=0) + +Reading pool state via StateView +--------------------------------- + +The StateView contract gives read-only access to pool state without making a +swap. + +.. code:: python + + from uniswap.constants import ETH_ADDRESS, ZERO_HOOK + + USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + + # Current pool price and tick + slot0 = uni.stateview_get_slot0(ETH_ADDRESS, USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK) + print(f"sqrtPriceX96: {slot0['sqrtPriceX96']}") + print(f"current tick: {slot0['tick']}") + + # Total in-range liquidity + liquidity = uni.stateview_get_liquidity(ETH_ADDRESS, USDC, fee=500, tick_spacing=10, hooks=ZERO_HOOK) + print(f"in-range liquidity: {liquidity}") diff --git a/examples/price_impact.py b/examples/price_impact.py new file mode 100644 index 00000000..1a0cdff9 --- /dev/null +++ b/examples/price_impact.py @@ -0,0 +1,61 @@ +from web3 import Web3 + +from uniswap import Uniswap +from uniswap.types import AddressLike + +eth = Web3.to_checksum_address("0x0000000000000000000000000000000000000000") +weth = Web3.to_checksum_address("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2") +usdt = Web3.to_checksum_address("0xdac17f958d2ee523a2206206994597c13d831ec7") +vxv = Web3.to_checksum_address("0x7d29a64504629172a429e64183d6673b9dacbfce") + + +def _perc(f: float) -> str: + return f"{round(f * 100, 3)}%" + + +def usdt_to_vxv_v2(): + """ + Checks impact for a pool with very little liquidity. + + This particular route caused a $14k loss for one user: https://github.com/uniswap-python/uniswap-python/discussions/198 + """ + uniswap = Uniswap(address=None, private_key=None, version=2) + + route: list[AddressLike] = [usdt, weth, vxv] + + # Compare the results with the output of: + # https://app.uniswap.org/#/swap?use=v2&inputCurrency=0xdac17f958d2ee523a2206206994597c13d831ec7&outputCurrency=0x7d29a64504629172a429e64183d6673b9dacbfce + qty = 10 * 10**8 + + # price = uniswap.get_price_input(usdt, vxv, qty, route=route) / 10 ** 18 + # print(price) + + impact = uniswap.estimate_price_impact(usdt, vxv, qty, fee=3000, route=route) + # NOTE: Not sure why this differs from the quote in the UI? + # Getting -27% in the UI for 10 USDT, but this returns >95% + # 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 + impact = uniswap.estimate_price_impact(usdt, vxv, qty, fee=3000, route=route) + print(f"Impact for buying VXV on v2 with {qty / 10**8} USDT: {_perc(impact)}") + + +def eth_to_vxv_v3(): + """Checks price impact for a pool with liquidity.""" + uniswap = Uniswap(address=None, private_key=None, version=3) + + # Compare the results with the output of: + # https://app.uniswap.org/#/swap?use=v3&inputCurrency=ETH&outputCurrency=0x7d29a64504629172a429e64183d6673b9dacbfce + 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 + 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)}") + + +if __name__ == "__main__": + usdt_to_vxv_v2() + eth_to_vxv_v3() diff --git a/examples/v4_examples.py b/examples/v4_examples.py new file mode 100644 index 00000000..8f907c80 --- /dev/null +++ b/examples/v4_examples.py @@ -0,0 +1,934 @@ +import os +from decimal import Decimal + +from colorama import Fore, Style +from web3 import Web3 + +from uniswap import Uniswap4, V4pools +from uniswap.types import PoolKey + + +def pool_tests(): + # pool list tests + first_block = 21688329 + print("Testing fetch_poolkey_data()...") + try: + v4pools_test.fetch_poolkey_data(first_block, chunk_size=5, clear_list=False) + print("Test passed.") + except Exception as e: + print(f"Test failed. {e}") + + print("Testing save_poolkeys_list() absolute path") + try: + v4pools_test.save_poolkeys_list( + os.path.join(_TESTS_DIR, "pools", "pool_list_mainnet.tst1") + ) + print("Test passed.") + except Exception as e: + print(f"Test failed. {e}") + + print("Testing load_poolkeys_list() absolute path") + try: + v4pools_test.load_poolkeys_list( + os.path.join(_TESTS_DIR, "pools", "pool_list_mainnet.tst1") + ) + print("Test passed.") + except Exception as e: + print(f"Test failed. {e}") + + print("Testing get_pool_key() for ETH-USDC (correct entry)...") + test_result = v4pools_test.get_poolkeys_sublist( + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "0x0000000000000000000000000000000000000000", + ) + print(test_result) + if len(test_result) > 0: + print("Test passed.") + else: + print("Test failed.") + + print("Testing get_pool_key() for 0x1-WETH (incorrect entry)") + test_result = v4pools_test.get_poolkeys_sublist( + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "0x0000000000000000000000000000000000000001", + ) + print(test_result) + if len(test_result) == 0: + print("Test passed.") + else: + print("Test failed.") + + print() + print() + + +def quoter_tests(): + # price functions tests + + # get_token_token_spot_price() tests + print(f"Testing getSlot0() for {Fore.GREEN}ETH-USDC{Style.RESET_ALL}") + test_result = str(uniV4_test.get_token_token_spot_price(test_ETH, test_USDC)) + print(f"Result: {Fore.GREEN}" + test_result + f"{Style.RESET_ALL}") + + print(f"Testing getSlot0() for {Fore.GREEN}USDC-ETH{Style.RESET_ALL}") + test_result = str(uniV4_test.get_token_token_spot_price(test_USDC, test_ETH)) + print(f"Result: {Fore.GREEN}" + test_result + f"{Style.RESET_ALL}") + print() + print() + + test_pool_key1 = PoolKey( + test_ETH, + test_USDC, + default_test_fee, + default_test_tick_spacing, + default_test_hooks, + ) + test_pool_key2 = PoolKey( + test_USDT, + test_USDC, + test_pool2_fee, + test_pool2_tick_spacing, + default_test_hooks, + ) + test_path_1hop: list = [] + test_path_1hop.append(test_pool_key1) + test_path_2hop: list = [] + test_path_2hop.append(test_pool_key1) + test_path_2hop.append(test_pool_key2) + + # Testing get_quote_exact_input_single() + test_volume = 1 + print( + "Testing exactInputSingle() for " + + str(test_volume) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDC{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.get_quote_exact_input_single( + test_ETH, + test_USDC, + test_volume * test_d0, + default_test_fee, + default_test_tick_spacing, + ) + / test_d1 + ) + test_result_alt = str( + uniV4_test.get_price_input( + test_ETH, + test_USDC, + test_volume * test_d0, + default_test_fee, + default_test_tick_spacing, + ) + / test_d1 + ) + print( + f"Result: {Fore.GREEN}" + + test_result + + " / " + + test_result_alt + + f"{Style.RESET_ALL} USDC" + ) + + test_volume = 3000 + print( + "Testing exactInputSingle() for " + + str(test_volume) + + f" {Fore.GREEN}USDC{Style.RESET_ALL} to {Fore.GREEN}ETH{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.get_quote_exact_input_single( + test_USDC, + test_ETH, + test_volume * test_d1, + default_test_fee, + default_test_tick_spacing, + ) + / test_d0 + ) + test_result_alt = str( + uniV4_test.get_price_input( + test_USDC, + test_ETH, + test_volume * test_d1, + default_test_fee, + default_test_tick_spacing, + ) + / test_d0 + ) + print( + f"Result: {Fore.GREEN}" + + test_result + + " / " + + test_result_alt + + f"{Style.RESET_ALL} ETH" + ) + print() + print() + + # Testing get_quote_exact_input() + test_volume = 1 + print( + "Testing exactInput() for " + + str(test_volume) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDC{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.get_quote_exact_input( + test_ETH, test_volume * test_d0, test_path_1hop + ) + / test_d1 + ) + test_result_alt = str( + uniV4_test.get_price_input( + test_ETH, test_USDC, test_volume * test_d0, route=test_path_1hop + ) + / test_d1 + ) + print( + f"Result: {Fore.GREEN}" + + test_result + + " / " + + test_result_alt + + f"{Style.RESET_ALL} USDC" + ) + + test_volume = 3000 + print( + "Testing exactInput() for " + + str(test_volume) + + f" {Fore.GREEN}USDC{Style.RESET_ALL} to {Fore.GREEN}ETH{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.get_quote_exact_input( + test_USDC, test_volume * test_d1, test_path_1hop + ) + / test_d0 + ) + test_result_alt = str( + uniV4_test.get_price_input( + test_USDC, test_ETH, test_volume * test_d1, route=test_path_1hop + ) + / test_d0 + ) + print( + f"Result: {Fore.GREEN}" + + test_result + + " / " + + test_result_alt + + f"{Style.RESET_ALL} ETH" + ) + + # 2-hop test + test_volume = 1 + print( + "Testing 2-hop exactInput() for " + + str(test_volume) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDT{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.get_quote_exact_input( + test_ETH, test_volume * test_d0, test_path_2hop + ) + / test_d2 + ) + test_result_alt = str( + uniV4_test.get_price_input( + test_ETH, test_USDT, test_volume * test_d0, route=test_path_2hop + ) + / test_d2 + ) + print( + f"Result: {Fore.GREEN}" + + test_result + + " / " + + test_result_alt + + f"{Style.RESET_ALL} USDT" + ) + + print() + print() + + # Testing get_quote_exact_output_single() + test_volume = 3000 + print( + "Testing exactOutputSingle() for " + + str(test_volume) + + f" {Fore.GREEN}USDC{Style.RESET_ALL} to {Fore.GREEN}ETH{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.get_quote_exact_output_single( + test_ETH, + test_USDC, + test_volume * test_d1, + default_test_fee, + default_test_tick_spacing, + ) + / test_d0 + ) + test_result_alt = str( + uniV4_test.get_price_output( + test_ETH, + test_USDC, + test_volume * test_d1, + default_test_fee, + default_test_tick_spacing, + ) + / test_d0 + ) + print( + f"Result: {Fore.GREEN}" + + test_result + + " / " + + test_result_alt + + f"{Style.RESET_ALL} ETH" + ) + + test_volume = 1 + print( + "Testing exactOutputSingle() for " + + str(test_volume) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDC{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.get_quote_exact_output_single( + test_USDC, + test_ETH, + test_volume * test_d0, + default_test_fee, + default_test_tick_spacing, + ) + / test_d1 + ) + test_result_alt = str( + uniV4_test.get_price_output( + test_USDC, + test_ETH, + test_volume * test_d0, + default_test_fee, + default_test_tick_spacing, + ) + / test_d1 + ) + print( + f"Result: {Fore.GREEN}" + + test_result + + " / " + + test_result_alt + + f"{Style.RESET_ALL} USDC" + ) + + # Testing get_quote_exact_output() + test_volume = 3000 + print( + "Testing exactOutput() one hop for " + + str(test_volume) + + f" {Fore.GREEN}USDC{Style.RESET_ALL} to {Fore.GREEN}ETH{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.get_quote_exact_output( + test_USDC, test_volume * test_d1, test_path_1hop + ) + / test_d0 + ) + test_result_alt = str( + uniV4_test.get_price_output( + test_USDC, test_ETH, test_volume * test_d1, route=test_path_1hop + ) + / test_d0 + ) + print( + f"Result: {Fore.GREEN}" + + test_result + + " / " + + test_result_alt + + f"{Style.RESET_ALL} ETH" + ) + + test_volume = 1 + print( + "Testing exactOutput() for " + + str(test_volume) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDC{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.get_quote_exact_output( + test_ETH, test_volume * test_d0, test_path_1hop + ) + / test_d1 + ) + test_result_alt = str( + uniV4_test.get_price_output( + test_ETH, test_USDC, test_volume * test_d0, route=test_path_1hop + ) + / test_d1 + ) + print( + f"Result: {Fore.GREEN}" + + test_result + + " / " + + test_result_alt + + f"{Style.RESET_ALL} USDC" + ) + + # 2-hop test + test_volume = 1 + print( + "Testing 2-hop exactOutput() for " + + str(test_volume) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDT{Style.RESET_ALL}" + ) + reversed_test_path_2hop = list(reversed(test_path_2hop)) + test_result = str( + uniV4_test.get_quote_exact_output( + test_ETH, test_volume * test_d0, reversed_test_path_2hop + ) + / test_d2 + ) + test_result_alt = str( + uniV4_test.get_price_output( + test_ETH, test_USDT, test_volume * test_d0, route=reversed_test_path_2hop + ) + / test_d2 + ) + + print( + f"Result: {Fore.GREEN}" + + test_result + + " / " + + test_result_alt + + f"{Style.RESET_ALL} USDT" + ) + + print() + print() + + +def price_impact_tests(): + ##estimate_price_impact() tests + test_volume = 1 + print( + "Testing estimate_price_impact() for " + + str(test_volume) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDC{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.estimate_price_impact(test_ETH, test_USDC, test_volume * test_d0) + ) + print(f"Result: {Fore.GREEN}" + test_result + f"{Style.RESET_ALL} %") + test_volume = 10 + print( + "Testing estimate_price_impact() for " + + str(test_volume) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDC{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.estimate_price_impact(test_ETH, test_USDC, test_volume * test_d0) + ) + print(f"Result: {Fore.GREEN}" + test_result + f"{Style.RESET_ALL} %") + test_volume = 100 + print( + "Testing estimate_price_impact() for " + + str(test_volume) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDC{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.estimate_price_impact(test_ETH, test_USDC, test_volume * test_d0) + ) + print(f"Result: {Fore.GREEN}" + test_result + f"{Style.RESET_ALL} %") + test_volume = 1000 + print( + "Testing estimate_price_impact() for " + + str(test_volume) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDC{Style.RESET_ALL}" + ) + test_result = str( + uniV4_test.estimate_price_impact(test_ETH, test_USDC, test_volume * test_d0) + ) + print(f"Result: {Fore.GREEN}" + test_result + f"{Style.RESET_ALL} %") + + print() + print() + + +def state_view_tests(): + ##StateView tests + # get_liquidity() test + print( + f"Testing get_liquidity() for ({Fore.GREEN}ETH{Style.RESET_ALL}, {Fore.GREEN}USDC{Style.RESET_ALL}) liquidity pool" + ) + test_result = uniV4_test.stateview_get_liquidity( + test_ETH, + test_USDC, + default_test_fee, + default_test_tick_spacing, + default_test_hooks, + ) + print(f"Result: {Fore.GREEN}" + str(test_result) + f"{Style.RESET_ALL}") + # get_slot0() test + print( + f"Testing get_slot0() for ({Fore.GREEN}ETH{Style.RESET_ALL}, {Fore.GREEN}USDC{Style.RESET_ALL}) liquidity pool" + ) + test_result1 = uniV4_test.stateview_get_slot0( + test_ETH, + test_USDC, + default_test_fee, + default_test_tick_spacing, + default_test_hooks, + ) + test_tick: int = int(test_result1["tick"]) + print(f"Result: {Fore.GREEN}" + str(test_result1) + f"{Style.RESET_ALL}") + # get_fee_growth_globals() test + print( + f"Testing get_fee_growth_globals() for ({Fore.GREEN}ETH{Style.RESET_ALL}, {Fore.GREEN}USDC{Style.RESET_ALL}) liquidity pool" + ) + test_result2 = uniV4_test.stateview_get_fee_growth_globals( + test_ETH, + test_USDC, + default_test_fee, + default_test_tick_spacing, + default_test_hooks, + ) + print(f"Result: {Fore.GREEN}" + str(test_result2) + f"{Style.RESET_ALL}") + # get_fee_growth_inside() test + print( + f"Testing get_fee_growth_inside() for ({Fore.GREEN}ETH{Style.RESET_ALL}, {Fore.GREEN}USDC{Style.RESET_ALL}) liquidity pool" + ) + test_result3 = uniV4_test.stateview_get_fee_growth_inside( + test_ETH, + test_USDC, + default_test_fee, + default_test_tick_spacing, + default_test_hooks, + test_tick - default_test_tick_spacing * 3, + test_tick + default_test_tick_spacing * 3, + ) + print(f"Result: {Fore.GREEN}" + str(test_result3) + f"{Style.RESET_ALL}") + # + # get_position_info_stateview() test + print( + f"Testing get_position_info_stateview() for ({Fore.GREEN}ETH{Style.RESET_ALL}, {Fore.GREEN}USDC{Style.RESET_ALL}) liquidity pool" + ) + # taken from 0xe529044f9cb8526c9b4d635f81889d991d217a6fb859b3e4f446cbe0ba988e31 as sample data + pos_inf_token0 = "0x6c76de483f1752ac8473e2b4983a873991e70da7" + pos_inf_token1 = "0xdac17f958d2ee523a2206206994597c13d831ec7" + pos_inf_fee = 13686 + pos_inf_tick_spacing = 10 + pos_inf_hooks = default_test_hooks + pos_inf_owner = "0x42562F062D618EfeB53cAB346E3b6E2EaB2e5BCB" + pos_inf_tick_lower = -301070 + pos_inf_tick_upper = -292859 + pos_inf_token_id = 156881 + test_result4 = uniV4_test.stateview_get_position_info( + pos_inf_token0, + pos_inf_token1, + pos_inf_fee, + pos_inf_tick_spacing, + pos_inf_hooks, + pos_inf_owner, + pos_inf_tick_lower, + pos_inf_tick_upper, + pos_inf_token_id, + ) + print(f"Result: {Fore.GREEN}" + str(test_result4) + f"{Style.RESET_ALL}") + # get_tick_bitmap_stateview() test + print( + f"Testing get_tick_bitmap()_stateview() for ({Fore.GREEN}ETH{Style.RESET_ALL}, {Fore.GREEN}USDC{Style.RESET_ALL}) liquidity pool" + ) + test_result5 = uniV4_test.stateview_get_tick_bitmap( + test_ETH, + test_USDC, + default_test_fee, + default_test_tick_spacing, + default_test_hooks, + -30000, # must be int16 + ) + print(f"Result: {Fore.GREEN}" + str(test_result5) + f"{Style.RESET_ALL}") + # get_tick_fee_growth_outside_stateview() test + print( + f"Testing get_tick_fee_growth_outside()_stateview() for ({Fore.GREEN}ETH{Style.RESET_ALL}, {Fore.GREEN}USDC{Style.RESET_ALL}) liquidity pool" + ) + test_result6 = uniV4_test.stateview_get_tick_fee_growth_outside( + test_ETH, + test_USDC, + default_test_fee, + default_test_tick_spacing, + default_test_hooks, + test_tick, + ) + print(f"Result: {Fore.GREEN}" + str(test_result6) + f"{Style.RESET_ALL}") + # get_tick_pool_info_stateview() test + print( + f"Testing get_tick_pool_info()_stateview() for ({Fore.GREEN}ETH{Style.RESET_ALL}, {Fore.GREEN}USDC{Style.RESET_ALL}) liquidity pool" + ) + test_result7 = uniV4_test.stateview_get_tick_pool_info( + test_ETH, + test_USDC, + default_test_fee, + default_test_tick_spacing, + default_test_hooks, + test_tick, + ) + print(f"Result: {Fore.GREEN}" + str(test_result7) + f"{Style.RESET_ALL}") + print() + print() + + +def swap_tests(): + # swap functions tests + + test_pool_key1 = PoolKey( + test_ETH, + test_USDC, + default_test_fee, + default_test_tick_spacing, + default_test_hooks, + ) + test_pool_key2 = PoolKey( + test_USDT, + test_USDC, + test_pool2_fee, + test_pool2_tick_spacing, + default_test_hooks, + ) + test_path_1hop: list = [] + test_path_1hop.append(test_pool_key1) + test_path_2hop: list = [] + test_path_2hop.append(test_pool_key1) + test_path_2hop.append(test_pool_key2) + + # Testing make_swap_input(), single hop, token0 is ETH + test_volume_in = 1 + print( + "Testing make_swap_input() for " + + str(test_volume_in) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDC{Style.RESET_ALL}, single hop" + ) + test_volume_out = uniV4_test.get_price_input( + test_ETH, + test_USDC, + test_volume_in * test_d0, + test_pool_key1.fee, + test_pool_key1.tick_spacing, + test_pool_key1.hooks, + ) + test_volume_out_min: int = int( + test_volume_out * Decimal(1 - uniV4_test.max_slippage) + ) + + test_result = uniV4_test.make_swap_input( + test_ETH, + test_USDC, + test_volume_in * test_d0, + test_volume_out_min, + test_pool_key1, + ) + print(f"Result: {Fore.GREEN}" + test_result.hex() + f"{Style.RESET_ALL}") + + # Testing make_swap_input(), single hop, token0 is non-ETH + test_volume_in = 1 + print( + "Testing make_swap_input() for " + + str(test_volume_in) + + f" {Fore.GREEN}USDC{Style.RESET_ALL} to {Fore.GREEN}ETH{Style.RESET_ALL}, single hop" + ) + test_volume_out = uniV4_test.get_price_input( + test_USDC, + test_ETH, + test_volume_in * test_d1, + test_pool_key1.fee, + test_pool_key1.tick_spacing, + test_pool_key1.hooks, + ) + test_volume_out_min = int(test_volume_out * Decimal(1 - uniV4_test.max_slippage)) + + test_result = uniV4_test.make_swap_input( + test_USDC, + test_ETH, + test_volume_in * test_d1, + test_volume_out_min, + test_pool_key1, + ) + print(f"Result: {Fore.GREEN}" + test_result.hex() + f"{Style.RESET_ALL}") + print() + print() + + # Testing make_swap_input(), 2-hop test, token0 is ETH + test_volume_in = 1 + print( + "Testing 2-hop make_swap_input() for " + + str(test_volume_in) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDT{Style.RESET_ALL}, 2-hop" + ) + test_volume_out = uniV4_test.get_price_input( + test_ETH, test_USDT, test_volume_in * test_d0, route=test_path_2hop + ) + test_volume_out_min = int(test_volume_out * Decimal(1 - uniV4_test.max_slippage)) + + test_result = uniV4_test.make_swap_input( + test_ETH, + test_USDT, + test_volume_in * test_d0, + test_volume_out_min, + route=test_path_2hop, + ) + print(f"Result: {Fore.GREEN}" + test_result.hex() + f"{Style.RESET_ALL}") + + # Testing make_swap_input(), 2-hop test, token0 is non-ETH + test_volume_in = 1 + print( + "Testing 2-hop make_swap_input() for " + + str(test_volume_in) + + f" {Fore.GREEN}USDT{Style.RESET_ALL} to {Fore.GREEN}ETH{Style.RESET_ALL}, 2-hop" + ) + test_volume_out = uniV4_test.get_price_input( + test_USDT, + test_ETH, + test_volume_in * test_d1, + route=list(reversed(test_path_2hop)), + ) + test_volume_out_min = int(test_volume_out * Decimal(1 - uniV4_test.max_slippage)) + test_result = uniV4_test.make_swap_input( + test_USDT, + test_ETH, + test_volume_in * test_d1, + test_volume_out_min, + route=list(reversed(test_path_2hop)), + ) + print(f"Result: {Fore.GREEN}" + test_result.hex() + f"{Style.RESET_ALL}") + + print() + print() + + # Testing make_swap_output(), single hop, token0 is ETH + test_volume_out = 1 + print( + "Testing make_swap_output() for " + + str(test_volume_out) + + f" {Fore.GREEN}USDC{Style.RESET_ALL} to {Fore.GREEN}ETH{Style.RESET_ALL}, single hop" + ) + test_volume_in = uniV4_test.get_price_output( + test_ETH, + test_USDC, + test_volume_out * test_d1, + test_pool_key1.fee, + test_pool_key1.tick_spacing, + ) + test_volume_in_max = int(test_volume_in * Decimal(1 + uniV4_test.max_slippage)) + test_result = uniV4_test.make_swap_output( + test_ETH, + test_USDC, + test_volume_out * test_d1, + test_volume_in_max, + test_pool_key1, + ) + print(f"Result: {Fore.GREEN}" + test_result.hex() + f"{Style.RESET_ALL}") + + # Testing make_swap_output(), single hop, token0 is non-ETH + test_volume_out = 1 + print( + "Testing make_swap_output() for " + + str(test_volume_out) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDC{Style.RESET_ALL}, single hop" + ) + test_volume_in = uniV4_test.get_price_output( + test_USDC, + test_ETH, + test_volume_out * test_d1, + test_pool_key1.fee, + test_pool_key1.tick_spacing, + ) + test_volume_in_max = int(test_volume_in * Decimal(1 + uniV4_test.max_slippage)) + test_result = uniV4_test.make_swap_output( + test_USDC, + test_ETH, + test_volume_out * test_d1, + test_volume_in_max, + test_pool_key1, + ) + print(f"Result: {Fore.GREEN}" + test_result.hex() + f"{Style.RESET_ALL}") + + # Testing make_swap_output(), 2-hop, token0 is ETH + test_volume_out = 1 + print( + "Testing 2-hop exactOutput() for " + + str(test_volume_out) + + f" {Fore.GREEN}ETH{Style.RESET_ALL} to {Fore.GREEN}USDT{Style.RESET_ALL}, 2-hop" + ) + reversed_test_path_2hop = list(reversed(test_path_2hop)) + test_volume_in = uniV4_test.get_price_output( + test_ETH, test_USDT, test_volume_out * test_d0, route=reversed_test_path_2hop + ) + test_volume_in_max = int(test_volume_in * Decimal(1 + uniV4_test.max_slippage)) + test_result = uniV4_test.make_swap_output( + test_ETH, + test_USDT, + test_volume_out * test_d2, + test_volume_in_max, + route=reversed_test_path_2hop, + ) + + print(f"Result: {Fore.GREEN}" + test_result.hex() + f"{Style.RESET_ALL}") + + # Testing make_swap_output(), 2-hop, token0 is non-ETH + test_volume_out = 1 + print( + "Testing 2-hop exactOutput() for " + + str(test_volume_out) + + f" {Fore.GREEN}USDT{Style.RESET_ALL} to {Fore.GREEN}ETH{Style.RESET_ALL}, 2-hop" + ) + test_volume_in = uniV4_test.get_price_output( + test_USDT, test_ETH, test_volume_out * test_d0, route=test_path_2hop + ) + test_volume_in_max = int(test_volume_in * Decimal(1 + uniV4_test.max_slippage)) + + test_result = uniV4_test.make_swap_output( + test_USDT, + test_ETH, + test_volume_out * test_d2, + test_volume_in_max, + route=test_path_2hop, + ) + + print(f"Result: {Fore.GREEN}" + test_result.hex() + f"{Style.RESET_ALL}") + + print() + print() + + +def liquidity_tests(): + ##liquidity management functions tests + # get_position_info() test + test_token_id: int = 1 + print( + f"Testing get_position_info() for token ID {Fore.GREEN}{test_token_id} {Style.RESET_ALL}" + ) + test_result1 = uniV4_test.get_position_info(test_token_id) + test_pool_id_result: int = int.from_bytes(test_result1["poolID"], byteorder="big") + test_pool_id_check = uniV4_test.get_pool_id( + PoolKey( + test_result1["currency0"], + test_result1["currency1"], + test_result1["fee"], + test_result1["tickSpacing"], + test_result1["hooks"], + ) + ) + print( + "Result: " + + str(test_result1) + + f"; truncated pool ID: {Fore.GREEN}" + + hex(test_pool_id_result) + + f"{Style.RESET_ALL}" + + f"; full pool ID: {Fore.GREEN}" + + f"{test_pool_id_check.hex()}" + + f"{Style.RESET_ALL}" + ) + print() + print() + + # get_position_info() test + print( + f"Testing get_position_value() for token ID {Fore.GREEN}{test_token_id} {Style.RESET_ALL}" + ) + test_result1 = uniV4_test.get_position_value(test_token_id, 18, 6) + print("Result: " + str(test_result1)) + print() + print() + + test_token_id = 206788 + print( + f"Testing get_position_info() for token ID {Fore.GREEN}{test_token_id} {Style.RESET_ALL}" + ) + test_result1 = uniV4_test.get_position_value(test_token_id, 18, 8) + print("Result: " + str(test_result1)) + print() + print() + + test_transaction_hash: str = ( + "0xb30d3dde98f715e5880da9f8833f99823623229e193e04661cb7ce193e4028f8" + ) + + print( + f"Testing get_minted_token_id() for transaction {Fore.GREEN}{test_transaction_hash} {Style.RESET_ALL}" + ) + test_result_token_id = uniV4_test.get_minted_token_id(test_transaction_hash) + print("Result: " + str(test_result_token_id)) + print() + print() + + test_transaction_hash = ( + "0xfe0389d167acbe1bb10f2ef0487ae123beab8a5b334799d68632957e3d16ff6e" + ) + + print( + f"Testing get_minted_token_id() for transaction {Fore.GREEN}{test_transaction_hash} {Style.RESET_ALL}" + ) + test_result_token_id = uniV4_test.get_minted_token_id(test_transaction_hash) + print("Result: " + str(test_result_token_id)) + print() + print() + + +def reserves_lens_tests(): + ##reserves lens tests + print( + f"Testing reserves_lens_get_pool_tvl() for ({Fore.GREEN}ETH{Style.RESET_ALL}, {Fore.GREEN}USDC{Style.RESET_ALL}) liquidity pool" + ) + test_result = uniV4_test.reserves_lens_get_pool_tvl( + PoolKey( + test_ETH, + test_USDC, + default_test_fee, + default_test_tick_spacing, + default_test_hooks, + ) + ) + + print("Result: " + str(test_result)) + print() + print() + + +if __name__ == "__main__": + test_ETH = "0x0000000000000000000000000000000000000000" + test_USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + test_USDT = Web3.to_checksum_address("0xdac17f958d2ee523a2206206994597c13d831ec7") + test_zero_hook = "0x0000000000000000000000000000000000000000" + test_d0 = 10**18 + test_d1 = 10**6 + test_d2 = 10**6 + test_fee = 500 + default_test_fee = 500 + default_test_tick_spacing = 10 + test_pool2_fee = 10 + test_pool2_tick_spacing = 1 + default_test_hooks = test_zero_hook + _TESTS_DIR = os.path.dirname(os.path.abspath(__file__)) + + rpc_endpoint = "https://eth.drpc.org" + address = "0x94e3361495bD110114ac0b6e35Ed75E77E6a6cFA" + w3_test = Web3(Web3.HTTPProvider(rpc_endpoint, request_kwargs={"timeout": 60})) + + uniV4_test = Uniswap4( + address, + None, + provider=rpc_endpoint, + ) + v4pools_test = V4pools(w3_test) + + ##TESTS + print("Started.") + print() + print() + + pool_tests() + quoter_tests() + price_impact_tests() + state_view_tests() + liquidity_tests() + reserves_lens_tests() + # swap_tests() + + print("Done.") diff --git a/metrics/adoption-2026-09-02.json b/metrics/adoption-2026-09-02.json new file mode 100644 index 00000000..8bf8aa7e --- /dev/null +++ b/metrics/adoption-2026-09-02.json @@ -0,0 +1,62 @@ +{ + "snapshot_at": "2026-09-02T01:29:29.972374+00:00", + "repo": "uniswap-python/uniswap-python", + "package": "uniswap-python", + "github": { + "stars": 1011, + "forks": 375, + "watchers": 23, + "open_issues": 66 + }, + "github_traffic": { + "note": "Skipped \u2014 set ADOPTION_GITHUB_TOKEN (PAT with Administration:read) to capture traffic (14-day window only). The default Actions GITHUB_TOKEN cannot read /traffic/*." + }, + "pypi": { + "downloads_last_day": 19, + "downloads_last_week": 210, + "downloads_last_month": 1044, + "version_split_note": "Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results." + }, + "pypi_versions": [ + { + "version": "0.8.0", + "released": "2026-08-03T09:45:06" + }, + { + "version": "0.7.1", + "released": "2023-09-09T12:22:42" + }, + { + "version": "0.7.0", + "released": "2023-03-30T13:21:25" + }, + { + "version": "0.6.2", + "released": "2022-11-08T13:36:42" + }, + { + "version": "0.6.1", + "released": "2022-10-18T12:25:29" + }, + { + "version": "0.6.0", + "released": "2022-08-29T10:03:46" + }, + { + "version": "0.5.5", + "released": "2022-01-03T09:02:11" + }, + { + "version": "0.5.4", + "released": "2021-12-21T09:53:51" + }, + { + "version": "0.5.3", + "released": "2021-10-19T10:34:58" + }, + { + "version": "0.5.2", + "released": "2021-06-23T13:58:55" + } + ] +} diff --git a/metrics/adoption-2026-09-02.md b/metrics/adoption-2026-09-02.md new file mode 100644 index 00000000..1879f453 --- /dev/null +++ b/metrics/adoption-2026-09-02.md @@ -0,0 +1,36 @@ +# uniswap-python Adoption Snapshot + +**Snapshot date**: 2026-09-02T01:29:29.972374+00:00 + +## GitHub + +| Metric | Value | +| --- | --- | +| Stars | 1011 | +| Forks | 375 | +| Watchers | 23 | +| Open Issues | 66 | + +## GitHub Traffic (14-day window) + +_Skipped — set ADOPTION_GITHUB_TOKEN (PAT with Administration:read) to capture traffic (14-day window only). The default Actions GITHUB_TOKEN cannot read /traffic/*._ + +## PyPI Downloads + +| Period | Downloads | +| --- | --- | +| Last day | 19 | +| Last week | 210 | +| Last month | 1044 | + +_Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results._ + +## Recent Releases + +| Version | Released | +| --- | --- | +| 0.8.0 | 2026-08-03 | +| 0.7.1 | 2023-09-09 | +| 0.7.0 | 2023-03-30 | +| 0.6.2 | 2022-11-08 | +| 0.6.1 | 2022-10-18 | diff --git a/metrics/adoption-2026-10-12.json b/metrics/adoption-2026-10-12.json new file mode 100644 index 00000000..3b6f14e9 --- /dev/null +++ b/metrics/adoption-2026-10-12.json @@ -0,0 +1,209 @@ +{ + "snapshot_at": "2026-10-12T15:02:45.752140+00:00", + "repo": "uniswap-python/uniswap-python", + "package": "uniswap-python", + "github": { + "stars": 1017, + "forks": 372, + "watchers": 23, + "open_issues": 66 + }, + "github_traffic": { + "clones_14d": 785, + "unique_cloners_14d": 179, + "views_14d": 1006, + "unique_visitors_14d": 209, + "clones_daily": [ + { + "date": "2026-09-28", + "count": 44, + "uniques": 16 + }, + { + "date": "2026-09-29", + "count": 16, + "uniques": 15 + }, + { + "date": "2026-09-30", + "count": 25, + "uniques": 13 + }, + { + "date": "2026-10-01", + "count": 52, + "uniques": 11 + }, + { + "date": "2026-10-02", + "count": 68, + "uniques": 16 + }, + { + "date": "2026-10-03", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-04", + "count": 24, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 78, + "uniques": 16 + }, + { + "date": "2026-10-06", + "count": 100, + "uniques": 18 + }, + { + "date": "2026-10-07", + "count": 19, + "uniques": 14 + }, + { + "date": "2026-10-08", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-09", + "count": 154, + "uniques": 18 + }, + { + "date": "2026-10-10", + "count": 23, + "uniques": 19 + }, + { + "date": "2026-10-11", + "count": 160, + "uniques": 28 + } + ], + "views_daily": [ + { + "date": "2026-09-28", + "count": 33, + "uniques": 9 + }, + { + "date": "2026-09-29", + "count": 34, + "uniques": 9 + }, + { + "date": "2026-09-30", + "count": 209, + "uniques": 37 + }, + { + "date": "2026-10-01", + "count": 57, + "uniques": 25 + }, + { + "date": "2026-10-02", + "count": 89, + "uniques": 22 + }, + { + "date": "2026-10-03", + "count": 19, + "uniques": 12 + }, + { + "date": "2026-10-04", + "count": 50, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 87, + "uniques": 19 + }, + { + "date": "2026-10-06", + "count": 51, + "uniques": 20 + }, + { + "date": "2026-10-07", + "count": 28, + "uniques": 18 + }, + { + "date": "2026-10-08", + "count": 124, + "uniques": 24 + }, + { + "date": "2026-10-09", + "count": 16, + "uniques": 7 + }, + { + "date": "2026-10-10", + "count": 31, + "uniques": 10 + }, + { + "date": "2026-10-11", + "count": 178, + "uniques": 45 + } + ] + }, + "pypi": { + "downloads_last_day": 37, + "downloads_last_week": 171, + "downloads_last_month": 609, + "version_split_note": "Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results." + }, + "pypi_versions": [ + { + "version": "0.8.0", + "released": "2026-08-03T09:45:06" + }, + { + "version": "0.7.1", + "released": "2023-09-09T12:22:42" + }, + { + "version": "0.7.0", + "released": "2023-03-30T13:21:25" + }, + { + "version": "0.6.2", + "released": "2022-11-08T13:36:42" + }, + { + "version": "0.6.1", + "released": "2022-10-18T12:25:29" + }, + { + "version": "0.6.0", + "released": "2022-08-29T10:03:46" + }, + { + "version": "0.5.5", + "released": "2022-01-03T09:02:11" + }, + { + "version": "0.5.4", + "released": "2021-12-21T09:53:51" + }, + { + "version": "0.5.3", + "released": "2021-10-19T10:34:58" + }, + { + "version": "0.5.2", + "released": "2021-06-23T13:58:55" + } + ] +} diff --git a/metrics/adoption-2026-10-12.md b/metrics/adoption-2026-10-12.md new file mode 100644 index 00000000..3cf010be --- /dev/null +++ b/metrics/adoption-2026-10-12.md @@ -0,0 +1,42 @@ +# uniswap-python Adoption Snapshot + +**Snapshot date**: 2026-10-12T15:02:45.752140+00:00 + +## GitHub + +| Metric | Value | +| --- | --- | +| Stars | 1017 | +| Forks | 372 | +| Watchers | 23 | +| Open Issues | 66 | + +## GitHub Traffic (14-day window) + +| Metric | Value | +| --- | --- | +| Clones (14d) | 785 | +| Unique Cloners (14d) | 179 | +| Views (14d) | 1006 | +| Unique Visitors (14d) | 209 | +| Daily series stored | 14 clone-days / 14 view-days | + +## PyPI Downloads + +| Period | Downloads | +| --- | --- | +| Last day | 37 | +| Last week | 171 | +| Last month | 609 | + +_Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results._ + +## Recent Releases + +| Version | Released | +| --- | --- | +| 0.8.0 | 2026-08-03 | +| 0.7.1 | 2023-09-09 | +| 0.7.0 | 2023-03-30 | +| 0.6.2 | 2022-11-08 | +| 0.6.1 | 2022-10-18 | diff --git a/metrics/adoption-2026-10-13.json b/metrics/adoption-2026-10-13.json new file mode 100644 index 00000000..07b01f9c --- /dev/null +++ b/metrics/adoption-2026-10-13.json @@ -0,0 +1,209 @@ +{ + "snapshot_at": "2026-10-13T13:57:20.730893+00:00", + "repo": "uniswap-python/uniswap-python", + "package": "uniswap-python", + "github": { + "stars": 1017, + "forks": 372, + "watchers": 23, + "open_issues": 66 + }, + "github_traffic": { + "clones_14d": 873, + "unique_cloners_14d": 223, + "views_14d": 1037, + "unique_visitors_14d": 218, + "clones_daily": [ + { + "date": "2026-09-29", + "count": 16, + "uniques": 15 + }, + { + "date": "2026-09-30", + "count": 25, + "uniques": 13 + }, + { + "date": "2026-10-01", + "count": 52, + "uniques": 11 + }, + { + "date": "2026-10-02", + "count": 68, + "uniques": 16 + }, + { + "date": "2026-10-03", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-04", + "count": 24, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 78, + "uniques": 16 + }, + { + "date": "2026-10-06", + "count": 100, + "uniques": 18 + }, + { + "date": "2026-10-07", + "count": 19, + "uniques": 14 + }, + { + "date": "2026-10-08", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-09", + "count": 154, + "uniques": 18 + }, + { + "date": "2026-10-10", + "count": 23, + "uniques": 19 + }, + { + "date": "2026-10-11", + "count": 160, + "uniques": 28 + }, + { + "date": "2026-10-12", + "count": 132, + "uniques": 58 + } + ], + "views_daily": [ + { + "date": "2026-09-29", + "count": 34, + "uniques": 9 + }, + { + "date": "2026-09-30", + "count": 209, + "uniques": 37 + }, + { + "date": "2026-10-01", + "count": 57, + "uniques": 25 + }, + { + "date": "2026-10-02", + "count": 89, + "uniques": 22 + }, + { + "date": "2026-10-03", + "count": 19, + "uniques": 12 + }, + { + "date": "2026-10-04", + "count": 50, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 87, + "uniques": 19 + }, + { + "date": "2026-10-06", + "count": 51, + "uniques": 20 + }, + { + "date": "2026-10-07", + "count": 28, + "uniques": 18 + }, + { + "date": "2026-10-08", + "count": 124, + "uniques": 24 + }, + { + "date": "2026-10-09", + "count": 16, + "uniques": 7 + }, + { + "date": "2026-10-10", + "count": 31, + "uniques": 10 + }, + { + "date": "2026-10-11", + "count": 178, + "uniques": 45 + }, + { + "date": "2026-10-12", + "count": 64, + "uniques": 17 + } + ] + }, + "pypi": { + "downloads_last_day": 13, + "downloads_last_week": 168, + "downloads_last_month": 609, + "version_split_note": "Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results." + }, + "pypi_versions": [ + { + "version": "0.8.0", + "released": "2026-08-03T09:45:06" + }, + { + "version": "0.7.1", + "released": "2023-09-09T12:22:42" + }, + { + "version": "0.7.0", + "released": "2023-03-30T13:21:25" + }, + { + "version": "0.6.2", + "released": "2022-11-08T13:36:42" + }, + { + "version": "0.6.1", + "released": "2022-10-18T12:25:29" + }, + { + "version": "0.6.0", + "released": "2022-08-29T10:03:46" + }, + { + "version": "0.5.5", + "released": "2022-01-03T09:02:11" + }, + { + "version": "0.5.4", + "released": "2021-12-21T09:53:51" + }, + { + "version": "0.5.3", + "released": "2021-10-19T10:34:58" + }, + { + "version": "0.5.2", + "released": "2021-06-23T13:58:55" + } + ] +} diff --git a/metrics/adoption-2026-10-13.md b/metrics/adoption-2026-10-13.md new file mode 100644 index 00000000..0b142a00 --- /dev/null +++ b/metrics/adoption-2026-10-13.md @@ -0,0 +1,42 @@ +# uniswap-python Adoption Snapshot + +**Snapshot date**: 2026-10-13T13:57:20.730893+00:00 + +## GitHub + +| Metric | Value | +| --- | --- | +| Stars | 1017 | +| Forks | 372 | +| Watchers | 23 | +| Open Issues | 66 | + +## GitHub Traffic (14-day window) + +| Metric | Value | +| --- | --- | +| Clones (14d) | 873 | +| Unique Cloners (14d) | 223 | +| Views (14d) | 1037 | +| Unique Visitors (14d) | 218 | +| Daily series stored | 14 clone-days / 14 view-days | + +## PyPI Downloads + +| Period | Downloads | +| --- | --- | +| Last day | 13 | +| Last week | 168 | +| Last month | 609 | + +_Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results._ + +## Recent Releases + +| Version | Released | +| --- | --- | +| 0.8.0 | 2026-08-03 | +| 0.7.1 | 2023-09-09 | +| 0.7.0 | 2023-03-30 | +| 0.6.2 | 2022-11-08 | +| 0.6.1 | 2022-10-18 | diff --git a/metrics/adoption-2026-10-14.json b/metrics/adoption-2026-10-14.json new file mode 100644 index 00000000..76aeec7e --- /dev/null +++ b/metrics/adoption-2026-10-14.json @@ -0,0 +1,209 @@ +{ + "snapshot_at": "2026-10-14T11:34:20.494530+00:00", + "repo": "uniswap-python/uniswap-python", + "package": "uniswap-python", + "github": { + "stars": 1017, + "forks": 373, + "watchers": 23, + "open_issues": 69 + }, + "github_traffic": { + "clones_14d": 930, + "unique_cloners_14d": 230, + "views_14d": 1275, + "unique_visitors_14d": 252, + "clones_daily": [ + { + "date": "2026-09-30", + "count": 25, + "uniques": 13 + }, + { + "date": "2026-10-01", + "count": 52, + "uniques": 11 + }, + { + "date": "2026-10-02", + "count": 68, + "uniques": 16 + }, + { + "date": "2026-10-03", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-04", + "count": 24, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 78, + "uniques": 16 + }, + { + "date": "2026-10-06", + "count": 100, + "uniques": 18 + }, + { + "date": "2026-10-07", + "count": 19, + "uniques": 14 + }, + { + "date": "2026-10-08", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-09", + "count": 154, + "uniques": 18 + }, + { + "date": "2026-10-10", + "count": 23, + "uniques": 19 + }, + { + "date": "2026-10-11", + "count": 160, + "uniques": 28 + }, + { + "date": "2026-10-12", + "count": 132, + "uniques": 58 + }, + { + "date": "2026-10-13", + "count": 73, + "uniques": 28 + } + ], + "views_daily": [ + { + "date": "2026-09-30", + "count": 209, + "uniques": 37 + }, + { + "date": "2026-10-01", + "count": 57, + "uniques": 25 + }, + { + "date": "2026-10-02", + "count": 89, + "uniques": 22 + }, + { + "date": "2026-10-03", + "count": 19, + "uniques": 12 + }, + { + "date": "2026-10-04", + "count": 50, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 87, + "uniques": 19 + }, + { + "date": "2026-10-06", + "count": 51, + "uniques": 20 + }, + { + "date": "2026-10-07", + "count": 28, + "uniques": 18 + }, + { + "date": "2026-10-08", + "count": 124, + "uniques": 24 + }, + { + "date": "2026-10-09", + "count": 16, + "uniques": 7 + }, + { + "date": "2026-10-10", + "count": 31, + "uniques": 10 + }, + { + "date": "2026-10-11", + "count": 178, + "uniques": 45 + }, + { + "date": "2026-10-12", + "count": 64, + "uniques": 17 + }, + { + "date": "2026-10-13", + "count": 272, + "uniques": 46 + } + ] + }, + "pypi": { + "downloads_last_day": 43, + "downloads_last_week": 161, + "downloads_last_month": 609, + "version_split_note": "Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results." + }, + "pypi_versions": [ + { + "version": "0.8.0", + "released": "2026-08-03T09:45:06" + }, + { + "version": "0.7.1", + "released": "2023-09-09T12:22:42" + }, + { + "version": "0.7.0", + "released": "2023-03-30T13:21:25" + }, + { + "version": "0.6.2", + "released": "2022-11-08T13:36:42" + }, + { + "version": "0.6.1", + "released": "2022-10-18T12:25:29" + }, + { + "version": "0.6.0", + "released": "2022-08-29T10:03:46" + }, + { + "version": "0.5.5", + "released": "2022-01-03T09:02:11" + }, + { + "version": "0.5.4", + "released": "2021-12-21T09:53:51" + }, + { + "version": "0.5.3", + "released": "2021-10-19T10:34:58" + }, + { + "version": "0.5.2", + "released": "2021-06-23T13:58:55" + } + ] +} diff --git a/metrics/adoption-2026-10-14.md b/metrics/adoption-2026-10-14.md new file mode 100644 index 00000000..8d5e490c --- /dev/null +++ b/metrics/adoption-2026-10-14.md @@ -0,0 +1,42 @@ +# uniswap-python Adoption Snapshot + +**Snapshot date**: 2026-10-14T11:34:20.494530+00:00 + +## GitHub + +| Metric | Value | +| --- | --- | +| Stars | 1017 | +| Forks | 373 | +| Watchers | 23 | +| Open Issues | 69 | + +## GitHub Traffic (14-day window) + +| Metric | Value | +| --- | --- | +| Clones (14d) | 930 | +| Unique Cloners (14d) | 230 | +| Views (14d) | 1275 | +| Unique Visitors (14d) | 252 | +| Daily series stored | 14 clone-days / 14 view-days | + +## PyPI Downloads + +| Period | Downloads | +| --- | --- | +| Last day | 43 | +| Last week | 161 | +| Last month | 609 | + +_Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results._ + +## Recent Releases + +| Version | Released | +| --- | --- | +| 0.8.0 | 2026-08-03 | +| 0.7.1 | 2023-09-09 | +| 0.7.0 | 2023-03-30 | +| 0.6.2 | 2022-11-08 | +| 0.6.1 | 2022-10-18 | diff --git a/metrics/adoption-2026-10-15.json b/metrics/adoption-2026-10-15.json new file mode 100644 index 00000000..3cc29d02 --- /dev/null +++ b/metrics/adoption-2026-10-15.json @@ -0,0 +1,209 @@ +{ + "snapshot_at": "2026-10-15T11:12:45.311248+00:00", + "repo": "uniswap-python/uniswap-python", + "package": "uniswap-python", + "github": { + "stars": 1017, + "forks": 373, + "watchers": 23, + "open_issues": 69 + }, + "github_traffic": { + "clones_14d": 1133, + "unique_cloners_14d": 246, + "views_14d": 1177, + "unique_visitors_14d": 245, + "clones_daily": [ + { + "date": "2026-10-01", + "count": 52, + "uniques": 11 + }, + { + "date": "2026-10-02", + "count": 68, + "uniques": 16 + }, + { + "date": "2026-10-03", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-04", + "count": 24, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 78, + "uniques": 16 + }, + { + "date": "2026-10-06", + "count": 100, + "uniques": 18 + }, + { + "date": "2026-10-07", + "count": 19, + "uniques": 14 + }, + { + "date": "2026-10-08", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-09", + "count": 154, + "uniques": 18 + }, + { + "date": "2026-10-10", + "count": 23, + "uniques": 19 + }, + { + "date": "2026-10-11", + "count": 160, + "uniques": 28 + }, + { + "date": "2026-10-12", + "count": 132, + "uniques": 58 + }, + { + "date": "2026-10-13", + "count": 73, + "uniques": 28 + }, + { + "date": "2026-10-14", + "count": 228, + "uniques": 33 + } + ], + "views_daily": [ + { + "date": "2026-10-01", + "count": 57, + "uniques": 25 + }, + { + "date": "2026-10-02", + "count": 89, + "uniques": 22 + }, + { + "date": "2026-10-03", + "count": 19, + "uniques": 12 + }, + { + "date": "2026-10-04", + "count": 50, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 87, + "uniques": 19 + }, + { + "date": "2026-10-06", + "count": 51, + "uniques": 20 + }, + { + "date": "2026-10-07", + "count": 28, + "uniques": 18 + }, + { + "date": "2026-10-08", + "count": 124, + "uniques": 24 + }, + { + "date": "2026-10-09", + "count": 16, + "uniques": 7 + }, + { + "date": "2026-10-10", + "count": 31, + "uniques": 10 + }, + { + "date": "2026-10-11", + "count": 178, + "uniques": 45 + }, + { + "date": "2026-10-12", + "count": 64, + "uniques": 17 + }, + { + "date": "2026-10-13", + "count": 272, + "uniques": 46 + }, + { + "date": "2026-10-14", + "count": 111, + "uniques": 24 + } + ] + }, + "pypi": { + "downloads_last_day": 12, + "downloads_last_week": 147, + "downloads_last_month": 609, + "version_split_note": "Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results." + }, + "pypi_versions": [ + { + "version": "0.8.0", + "released": "2026-08-03T09:45:06" + }, + { + "version": "0.7.1", + "released": "2023-09-09T12:22:42" + }, + { + "version": "0.7.0", + "released": "2023-03-30T13:21:25" + }, + { + "version": "0.6.2", + "released": "2022-11-08T13:36:42" + }, + { + "version": "0.6.1", + "released": "2022-10-18T12:25:29" + }, + { + "version": "0.6.0", + "released": "2022-08-29T10:03:46" + }, + { + "version": "0.5.5", + "released": "2022-01-03T09:02:11" + }, + { + "version": "0.5.4", + "released": "2021-12-21T09:53:51" + }, + { + "version": "0.5.3", + "released": "2021-10-19T10:34:58" + }, + { + "version": "0.5.2", + "released": "2021-06-23T13:58:55" + } + ] +} diff --git a/metrics/adoption-2026-10-15.md b/metrics/adoption-2026-10-15.md new file mode 100644 index 00000000..c3ba0e46 --- /dev/null +++ b/metrics/adoption-2026-10-15.md @@ -0,0 +1,42 @@ +# uniswap-python Adoption Snapshot + +**Snapshot date**: 2026-10-15T11:12:45.311248+00:00 + +## GitHub + +| Metric | Value | +| --- | --- | +| Stars | 1017 | +| Forks | 373 | +| Watchers | 23 | +| Open Issues | 69 | + +## GitHub Traffic (14-day window) + +| Metric | Value | +| --- | --- | +| Clones (14d) | 1133 | +| Unique Cloners (14d) | 246 | +| Views (14d) | 1177 | +| Unique Visitors (14d) | 245 | +| Daily series stored | 14 clone-days / 14 view-days | + +## PyPI Downloads + +| Period | Downloads | +| --- | --- | +| Last day | 12 | +| Last week | 147 | +| Last month | 609 | + +_Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results._ + +## Recent Releases + +| Version | Released | +| --- | --- | +| 0.8.0 | 2026-08-03 | +| 0.7.1 | 2023-09-09 | +| 0.7.0 | 2023-03-30 | +| 0.6.2 | 2022-11-08 | +| 0.6.1 | 2022-10-18 | diff --git a/metrics/adoption-2026-10-16.json b/metrics/adoption-2026-10-16.json new file mode 100644 index 00000000..60f1e67d --- /dev/null +++ b/metrics/adoption-2026-10-16.json @@ -0,0 +1,209 @@ +{ + "snapshot_at": "2026-10-16T11:07:20.499567+00:00", + "repo": "uniswap-python/uniswap-python", + "package": "uniswap-python", + "github": { + "stars": 1017, + "forks": 373, + "watchers": 23, + "open_issues": 69 + }, + "github_traffic": { + "clones_14d": 1138, + "unique_cloners_14d": 255, + "views_14d": 1145, + "unique_visitors_14d": 245, + "clones_daily": [ + { + "date": "2026-10-02", + "count": 68, + "uniques": 16 + }, + { + "date": "2026-10-03", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-04", + "count": 24, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 78, + "uniques": 16 + }, + { + "date": "2026-10-06", + "count": 100, + "uniques": 18 + }, + { + "date": "2026-10-07", + "count": 19, + "uniques": 14 + }, + { + "date": "2026-10-08", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-09", + "count": 154, + "uniques": 18 + }, + { + "date": "2026-10-10", + "count": 23, + "uniques": 19 + }, + { + "date": "2026-10-11", + "count": 160, + "uniques": 28 + }, + { + "date": "2026-10-12", + "count": 132, + "uniques": 58 + }, + { + "date": "2026-10-13", + "count": 73, + "uniques": 28 + }, + { + "date": "2026-10-14", + "count": 228, + "uniques": 33 + }, + { + "date": "2026-10-15", + "count": 57, + "uniques": 20 + } + ], + "views_daily": [ + { + "date": "2026-10-02", + "count": 89, + "uniques": 22 + }, + { + "date": "2026-10-03", + "count": 19, + "uniques": 12 + }, + { + "date": "2026-10-04", + "count": 50, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 87, + "uniques": 19 + }, + { + "date": "2026-10-06", + "count": 51, + "uniques": 20 + }, + { + "date": "2026-10-07", + "count": 28, + "uniques": 18 + }, + { + "date": "2026-10-08", + "count": 124, + "uniques": 24 + }, + { + "date": "2026-10-09", + "count": 16, + "uniques": 7 + }, + { + "date": "2026-10-10", + "count": 31, + "uniques": 10 + }, + { + "date": "2026-10-11", + "count": 178, + "uniques": 45 + }, + { + "date": "2026-10-12", + "count": 64, + "uniques": 17 + }, + { + "date": "2026-10-13", + "count": 272, + "uniques": 46 + }, + { + "date": "2026-10-14", + "count": 111, + "uniques": 24 + }, + { + "date": "2026-10-15", + "count": 25, + "uniques": 17 + } + ] + }, + "pypi": { + "downloads_last_day": 11, + "downloads_last_week": 155, + "downloads_last_month": 609, + "version_split_note": "Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results." + }, + "pypi_versions": [ + { + "version": "0.8.0", + "released": "2026-08-03T09:45:06" + }, + { + "version": "0.7.1", + "released": "2023-09-09T12:22:42" + }, + { + "version": "0.7.0", + "released": "2023-03-30T13:21:25" + }, + { + "version": "0.6.2", + "released": "2022-11-08T13:36:42" + }, + { + "version": "0.6.1", + "released": "2022-10-18T12:25:29" + }, + { + "version": "0.6.0", + "released": "2022-08-29T10:03:46" + }, + { + "version": "0.5.5", + "released": "2022-01-03T09:02:11" + }, + { + "version": "0.5.4", + "released": "2021-12-21T09:53:51" + }, + { + "version": "0.5.3", + "released": "2021-10-19T10:34:58" + }, + { + "version": "0.5.2", + "released": "2021-06-23T13:58:55" + } + ] +} diff --git a/metrics/adoption-2026-10-16.md b/metrics/adoption-2026-10-16.md new file mode 100644 index 00000000..91bbcb2e --- /dev/null +++ b/metrics/adoption-2026-10-16.md @@ -0,0 +1,42 @@ +# uniswap-python Adoption Snapshot + +**Snapshot date**: 2026-10-16T11:07:20.499567+00:00 + +## GitHub + +| Metric | Value | +| --- | --- | +| Stars | 1017 | +| Forks | 373 | +| Watchers | 23 | +| Open Issues | 69 | + +## GitHub Traffic (14-day window) + +| Metric | Value | +| --- | --- | +| Clones (14d) | 1138 | +| Unique Cloners (14d) | 255 | +| Views (14d) | 1145 | +| Unique Visitors (14d) | 245 | +| Daily series stored | 14 clone-days / 14 view-days | + +## PyPI Downloads + +| Period | Downloads | +| --- | --- | +| Last day | 11 | +| Last week | 155 | +| Last month | 609 | + +_Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results._ + +## Recent Releases + +| Version | Released | +| --- | --- | +| 0.8.0 | 2026-08-03 | +| 0.7.1 | 2023-09-09 | +| 0.7.0 | 2023-03-30 | +| 0.6.2 | 2022-11-08 | +| 0.6.1 | 2022-10-18 | diff --git a/metrics/adoption-2026-10-17.json b/metrics/adoption-2026-10-17.json new file mode 100644 index 00000000..9863c89c --- /dev/null +++ b/metrics/adoption-2026-10-17.json @@ -0,0 +1,209 @@ +{ + "snapshot_at": "2026-10-17T10:27:04.544968+00:00", + "repo": "uniswap-python/uniswap-python", + "package": "uniswap-python", + "github": { + "stars": 1017, + "forks": 373, + "watchers": 23, + "open_issues": 69 + }, + "github_traffic": { + "clones_14d": 1110, + "unique_cloners_14d": 273, + "views_14d": 1077, + "unique_visitors_14d": 235, + "clones_daily": [ + { + "date": "2026-10-03", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-04", + "count": 24, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 78, + "uniques": 16 + }, + { + "date": "2026-10-06", + "count": 100, + "uniques": 18 + }, + { + "date": "2026-10-07", + "count": 19, + "uniques": 14 + }, + { + "date": "2026-10-08", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-09", + "count": 154, + "uniques": 18 + }, + { + "date": "2026-10-10", + "count": 23, + "uniques": 19 + }, + { + "date": "2026-10-11", + "count": 160, + "uniques": 28 + }, + { + "date": "2026-10-12", + "count": 132, + "uniques": 58 + }, + { + "date": "2026-10-13", + "count": 73, + "uniques": 28 + }, + { + "date": "2026-10-14", + "count": 228, + "uniques": 33 + }, + { + "date": "2026-10-15", + "count": 57, + "uniques": 20 + }, + { + "date": "2026-10-16", + "count": 40, + "uniques": 30 + } + ], + "views_daily": [ + { + "date": "2026-10-03", + "count": 19, + "uniques": 12 + }, + { + "date": "2026-10-04", + "count": 50, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 87, + "uniques": 19 + }, + { + "date": "2026-10-06", + "count": 51, + "uniques": 20 + }, + { + "date": "2026-10-07", + "count": 28, + "uniques": 18 + }, + { + "date": "2026-10-08", + "count": 124, + "uniques": 24 + }, + { + "date": "2026-10-09", + "count": 16, + "uniques": 7 + }, + { + "date": "2026-10-10", + "count": 31, + "uniques": 10 + }, + { + "date": "2026-10-11", + "count": 178, + "uniques": 45 + }, + { + "date": "2026-10-12", + "count": 64, + "uniques": 17 + }, + { + "date": "2026-10-13", + "count": 272, + "uniques": 46 + }, + { + "date": "2026-10-14", + "count": 111, + "uniques": 24 + }, + { + "date": "2026-10-15", + "count": 25, + "uniques": 17 + }, + { + "date": "2026-10-16", + "count": 21, + "uniques": 11 + } + ] + }, + "pypi": { + "downloads_last_day": 49, + "downloads_last_week": 180, + "downloads_last_month": 609, + "version_split_note": "Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results." + }, + "pypi_versions": [ + { + "version": "0.8.0", + "released": "2026-08-03T09:45:06" + }, + { + "version": "0.7.1", + "released": "2023-09-09T12:22:42" + }, + { + "version": "0.7.0", + "released": "2023-03-30T13:21:25" + }, + { + "version": "0.6.2", + "released": "2022-11-08T13:36:42" + }, + { + "version": "0.6.1", + "released": "2022-10-18T12:25:29" + }, + { + "version": "0.6.0", + "released": "2022-08-29T10:03:46" + }, + { + "version": "0.5.5", + "released": "2022-01-03T09:02:11" + }, + { + "version": "0.5.4", + "released": "2021-12-21T09:53:51" + }, + { + "version": "0.5.3", + "released": "2021-10-19T10:34:58" + }, + { + "version": "0.5.2", + "released": "2021-06-23T13:58:55" + } + ] +} diff --git a/metrics/adoption-2026-10-17.md b/metrics/adoption-2026-10-17.md new file mode 100644 index 00000000..b6cccbe2 --- /dev/null +++ b/metrics/adoption-2026-10-17.md @@ -0,0 +1,42 @@ +# uniswap-python Adoption Snapshot + +**Snapshot date**: 2026-10-17T10:27:04.544968+00:00 + +## GitHub + +| Metric | Value | +| --- | --- | +| Stars | 1017 | +| Forks | 373 | +| Watchers | 23 | +| Open Issues | 69 | + +## GitHub Traffic (14-day window) + +| Metric | Value | +| --- | --- | +| Clones (14d) | 1110 | +| Unique Cloners (14d) | 273 | +| Views (14d) | 1077 | +| Unique Visitors (14d) | 235 | +| Daily series stored | 14 clone-days / 14 view-days | + +## PyPI Downloads + +| Period | Downloads | +| --- | --- | +| Last day | 49 | +| Last week | 180 | +| Last month | 609 | + +_Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results._ + +## Recent Releases + +| Version | Released | +| --- | --- | +| 0.8.0 | 2026-08-03 | +| 0.7.1 | 2023-09-09 | +| 0.7.0 | 2023-03-30 | +| 0.6.2 | 2022-11-08 | +| 0.6.1 | 2022-10-18 | diff --git a/metrics/adoption-2026-10-18.json b/metrics/adoption-2026-10-18.json new file mode 100644 index 00000000..d45fb6ed --- /dev/null +++ b/metrics/adoption-2026-10-18.json @@ -0,0 +1,209 @@ +{ + "snapshot_at": "2026-10-18T10:45:41.364618+00:00", + "repo": "uniswap-python/uniswap-python", + "package": "uniswap-python", + "github": { + "stars": 1016, + "forks": 373, + "watchers": 23, + "open_issues": 69 + }, + "github_traffic": { + "clones_14d": 1134, + "unique_cloners_14d": 285, + "views_14d": 1081, + "unique_visitors_14d": 240, + "clones_daily": [ + { + "date": "2026-10-04", + "count": 24, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 78, + "uniques": 16 + }, + { + "date": "2026-10-06", + "count": 100, + "uniques": 18 + }, + { + "date": "2026-10-07", + "count": 19, + "uniques": 14 + }, + { + "date": "2026-10-08", + "count": 11, + "uniques": 8 + }, + { + "date": "2026-10-09", + "count": 154, + "uniques": 18 + }, + { + "date": "2026-10-10", + "count": 23, + "uniques": 19 + }, + { + "date": "2026-10-11", + "count": 160, + "uniques": 28 + }, + { + "date": "2026-10-12", + "count": 132, + "uniques": 58 + }, + { + "date": "2026-10-13", + "count": 73, + "uniques": 28 + }, + { + "date": "2026-10-14", + "count": 228, + "uniques": 33 + }, + { + "date": "2026-10-15", + "count": 57, + "uniques": 20 + }, + { + "date": "2026-10-16", + "count": 40, + "uniques": 30 + }, + { + "date": "2026-10-17", + "count": 35, + "uniques": 23 + } + ], + "views_daily": [ + { + "date": "2026-10-04", + "count": 50, + "uniques": 18 + }, + { + "date": "2026-10-05", + "count": 87, + "uniques": 19 + }, + { + "date": "2026-10-06", + "count": 51, + "uniques": 20 + }, + { + "date": "2026-10-07", + "count": 28, + "uniques": 18 + }, + { + "date": "2026-10-08", + "count": 124, + "uniques": 24 + }, + { + "date": "2026-10-09", + "count": 16, + "uniques": 7 + }, + { + "date": "2026-10-10", + "count": 31, + "uniques": 10 + }, + { + "date": "2026-10-11", + "count": 178, + "uniques": 45 + }, + { + "date": "2026-10-12", + "count": 64, + "uniques": 17 + }, + { + "date": "2026-10-13", + "count": 272, + "uniques": 46 + }, + { + "date": "2026-10-14", + "count": 111, + "uniques": 24 + }, + { + "date": "2026-10-15", + "count": 25, + "uniques": 17 + }, + { + "date": "2026-10-16", + "count": 21, + "uniques": 11 + }, + { + "date": "2026-10-17", + "count": 23, + "uniques": 15 + } + ] + }, + "pypi": { + "downloads_last_day": 6, + "downloads_last_week": 171, + "downloads_last_month": 609, + "version_split_note": "Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results." + }, + "pypi_versions": [ + { + "version": "0.8.0", + "released": "2026-08-03T09:45:06" + }, + { + "version": "0.7.1", + "released": "2023-09-09T12:22:42" + }, + { + "version": "0.7.0", + "released": "2023-03-30T13:21:25" + }, + { + "version": "0.6.2", + "released": "2022-11-08T13:36:42" + }, + { + "version": "0.6.1", + "released": "2022-10-18T12:25:29" + }, + { + "version": "0.6.0", + "released": "2022-08-29T10:03:46" + }, + { + "version": "0.5.5", + "released": "2022-01-03T09:02:11" + }, + { + "version": "0.5.4", + "released": "2021-12-21T09:53:51" + }, + { + "version": "0.5.3", + "released": "2021-10-19T10:34:58" + }, + { + "version": "0.5.2", + "released": "2021-06-23T13:58:55" + } + ] +} diff --git a/metrics/adoption-2026-10-18.md b/metrics/adoption-2026-10-18.md new file mode 100644 index 00000000..68ce10b9 --- /dev/null +++ b/metrics/adoption-2026-10-18.md @@ -0,0 +1,42 @@ +# uniswap-python Adoption Snapshot + +**Snapshot date**: 2026-10-18T10:45:41.364618+00:00 + +## GitHub + +| Metric | Value | +| --- | --- | +| Stars | 1016 | +| Forks | 373 | +| Watchers | 23 | +| Open Issues | 69 | + +## GitHub Traffic (14-day window) + +| Metric | Value | +| --- | --- | +| Clones (14d) | 1134 | +| Unique Cloners (14d) | 285 | +| Views (14d) | 1081 | +| Unique Visitors (14d) | 240 | +| Daily series stored | 14 clone-days / 14 view-days | + +## PyPI Downloads + +| Period | Downloads | +| --- | --- | +| Last day | 6 | +| Last week | 171 | +| Last month | 609 | + +_Per-version counts require BigQuery (pypistats.org does not expose them). To check v4 pickup specifically, compare release-date-filtered BigQuery results._ + +## Recent Releases + +| Version | Released | +| --- | --- | +| 0.8.0 | 2026-08-03 | +| 0.7.1 | 2023-09-09 | +| 0.7.0 | 2023-03-30 | +| 0.6.2 | 2022-11-08 | +| 0.6.1 | 2022-10-18 | diff --git a/poetry.lock b/poetry.lock index 6a5e5e35..772ce5bb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,1211 +1,3033 @@ +# This file is automatically @generated by Poetry 2.4.0 and should not be changed by hand. + [[package]] -category = "dev" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -name = "appdirs" +name = "accessible-pygments" +version = "0.0.4" +description = "A collection of accessible pygments styles" optional = false python-versions = "*" -version = "1.4.4" +groups = ["dev"] +files = [ + {file = "accessible-pygments-0.0.4.tar.gz", hash = "sha256:e7b57a9b15958e9601c7e9eb07a440c813283545a20973f2574a5f453d0e953e"}, + {file = "accessible_pygments-0.0.4-py2.py3-none-any.whl", hash = "sha256:416c6d8c1ea1c5ad8701903a20fcedf953c6e720d64f33dc47bfb2d3f2fa4e8d"}, +] + +[package.dependencies] +pygments = ">=1.5" + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472"}, + {file = "aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d"}, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a"}, + {file = "aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479"}, + {file = "aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f"}, + {file = "aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32"}, + {file = "aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c"}, + {file = "aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a"}, + {file = "aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db"}, + {file = "aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910"}, + {file = "aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7"}, + {file = "aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d"}, + {file = "aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228"}, + {file = "aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42"}, + {file = "aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d"}, + {file = "aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19"}, + {file = "aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a"}, + {file = "aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86"}, + {file = "aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c"}, + {file = "aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0"}, + {file = "aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5"}, + {file = "aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e"}, + {file = "aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71"}, + {file = "aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883"}, + {file = "aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062"}, + {file = "aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7"}, + {file = "aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646"}, + {file = "aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147"}, + {file = "aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41"}, + {file = "aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf"}, + {file = "aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100"}, + {file = "aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85"}, + {file = "aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9"}, + {file = "aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b"}, + {file = "aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7"}, + {file = "aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +typing_extensions = {version = ">=4.4", markers = "python_version < \"3.13\""} +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\" and sys_platform != \"android\" and sys_platform != \"ios\"", "aiodns (>=3.3.0) ; sys_platform != \"android\" and sys_platform != \"ios\"", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\" and sys_platform != \"android\" and sys_platform != \"ios\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] [[package]] -category = "dev" -description = "Atomic file writes." -marker = "sys_platform == \"win32\"" -name = "atomicwrites" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +name = "aiosignal" version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} [[package]] -category = "main" -description = "Classes Without Boilerplate" -name = "attrs" +name = "alabaster" +version = "1.0.0" +description = "A light, configurable Sphinx theme" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "19.3.0" +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] -[package.extras] -azure-pipelines = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "pytest-azurepipelines"] -dev = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "pre-commit"] -docs = ["sphinx", "zope.interface"] -tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +[[package]] +name = "annotated-types" +version = "0.8.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0"}, + {file = "annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7"}, +] [[package]] -category = "main" -description = "Base58 and Base58Check implementation" -name = "base58" +name = "attrs" +version = "22.2.0" +description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.5" -version = "2.0.1" +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle ; platform_python_implementation == \"CPython\"", "cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990) ; platform_python_implementation == \"CPython\"", "mypy (>=0.971,<0.990) ; platform_python_implementation == \"CPython\"", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version < \"3.11\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version < \"3.11\"", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] [[package]] -category = "main" -description = "efficient arrays of booleans -- C extension" -name = "bitarray" +name = "babel" +version = "2.18.0" +description = "Internationalization utilities" optional = false -python-versions = "*" -version = "1.2.2" +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, +] + +[package.extras] +dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] [[package]] -category = "dev" -description = "The uncompromising code formatter." -name = "black" +name = "beautifulsoup4" +version = "4.12.0" +description = "Screen-scraping library" optional = false -python-versions = ">=3.6" -version = "19.10b0" +python-versions = ">=3.6.0" +groups = ["dev"] +files = [ + {file = "beautifulsoup4-4.12.0-py3-none-any.whl", hash = "sha256:2130a5ad7f513200fae61a17abb5e338ca980fa28c439c0571014bc0217e9591"}, + {file = "beautifulsoup4-4.12.0.tar.gz", hash = "sha256:c5fceeaec29d09c84970e47c65f2f0efe57872f7cff494c9691a26ec0ff13234"}, +] [package.dependencies] -appdirs = "*" -attrs = ">=18.1.0" -click = ">=6.5" -pathspec = ">=0.6,<1" -regex = "*" -toml = ">=0.9.4" -typed-ast = ">=1.4.0" +soupsieve = ">1.2" [package.extras] -d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +html5lib = ["html5lib"] +lxml = ["lxml"] [[package]] -category = "main" -description = "Python package for providing Mozilla's CA Bundle." -name = "certifi" +name = "bitarray" +version = "2.7.3" +description = "efficient arrays of booleans -- C extension" optional = false python-versions = "*" -version = "2020.6.20" +groups = ["main"] +files = [ + {file = "bitarray-2.7.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:979d42e0b2c3113526f9716a461e08671788a23ce7e3b5cd090ce3e6a6762641"}, + {file = "bitarray-2.7.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:860edf8533223d82bd6201894bcaf540f828f49075f363390eecf04b12fb94cb"}, + {file = "bitarray-2.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:78378d8dacbe1f4f263347f42ec0a41cc2097cd671c6ac30a65a838284a5e141"}, + {file = "bitarray-2.7.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:888df211aafe5fad41c0792a686d95c8ba37345d5037f437aa3c09608f9c3b56"}, + {file = "bitarray-2.7.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb3f003dee96dbf24a6df71443557f249b17b20083c189995302b14eb01530bf"}, + {file = "bitarray-2.7.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c531532c21bc1063e65957a1a85a2d13601ec21801f70821c89d9339b16ebc78"}, + {file = "bitarray-2.7.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8fd92c8026e4ba6874e94f538890e35bef2a3a18ea54e3663c578b7916ade1"}, + {file = "bitarray-2.7.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d19c34a2121eccfeb642d4ad71163bd3342a8f3a99e6724fe824bdfbc0a5b65"}, + {file = "bitarray-2.7.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:102db74ee82ec5774aba01481e73eedaebd27ba167344a81d3b42e6fbf9ffb77"}, + {file = "bitarray-2.7.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7f6540b45b2230442f7a0614745131e0a6f28251f5d33ac19d0ed61d80db7153"}, + {file = "bitarray-2.7.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:99c9345c417a9cff98f9f6e59b0350dcc10c2e0e1ea66acf7946de1cd60541fa"}, + {file = "bitarray-2.7.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:a1d439c98e65ab8e5fbcc2b242a16e7a3f076974bff78185ff42ba2d4c220032"}, + {file = "bitarray-2.7.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:87897ec0e4876c9f2c1ae313519de0ed2ad8041a4d2210a083f9b4a239add2e3"}, + {file = "bitarray-2.7.3-cp310-cp310-win32.whl", hash = "sha256:cb46c3a4002c8322dd0e1b4b53f8a647dcb0f199f5c7a1fc03d3880c3eabbd2c"}, + {file = "bitarray-2.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:5df10eb9b794932b0cf806f412d1c6d04fb7655ca7ae5caf6354b9edc380a5f7"}, + {file = "bitarray-2.7.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:27524bc92fdeb464a5057a4677a35f482cf30be2e920bd1d11c46de533cafda6"}, + {file = "bitarray-2.7.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cf37431de779b29e5c0d8e36868f77f6df53c3c19c20e8404137e257dc80040"}, + {file = "bitarray-2.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8abd23f94cdcce971d932a5f0a066d40fbc61901fd087aa70d32cccd1793bd20"}, + {file = "bitarray-2.7.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7659bdfe7716b14a39007e31e957fa64d7f0d9e40a1dbd024bd81b972d76bffb"}, + {file = "bitarray-2.7.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:da1570f301abdfda68f4fdb40c4d3f09af4bb6e4550b4fa5395db0d142b680bc"}, + {file = "bitarray-2.7.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8becbb9649fd29ee577f9f0405ce2fba5cf9fa2c290c9b044bc235c04473f213"}, + {file = "bitarray-2.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72fd7f6f940bc42914c86700591ccfd1daeff0e414cefcbd7843117df2fac4e9"}, + {file = "bitarray-2.7.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23b7bada6d6b62cba08f4a1b8a95da2d8592aae1db3c167dcb52abcba0a7bef5"}, + {file = "bitarray-2.7.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4b2d150a81a981537801ac7d4f4f5d082c48343612a21f4e2c4cd2e887973bd5"}, + {file = "bitarray-2.7.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1502660ab489b1f18c3493c766252cd5d24bc1cbf4bdf3594e0a30de142ed453"}, + {file = "bitarray-2.7.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:91f43f6b6c9129a56d3e2dccb8b88ffce0e4f4893dd9d69d285676bdf5b9ca14"}, + {file = "bitarray-2.7.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:a69c99274aee2ffdc7f1cfd34044ccb7155790d6f5217d677ea46a6ddead6dd2"}, + {file = "bitarray-2.7.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d63f20299441e32171f08fc62f7ea7e401cc12a96f67a36ab2d76439ecfcb118"}, + {file = "bitarray-2.7.3-cp311-cp311-win32.whl", hash = "sha256:0b84fd9dbf999cbca1090a7703aa1404cd01af4035c6ba3adf69d41280611fb6"}, + {file = "bitarray-2.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:76bbbb9ceebb9cbb2b14369b3681fecab226792b339f612e79f6575ca31fed45"}, + {file = "bitarray-2.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50d5e2c026b3e3d145f64c457338ea99edcbdd302fdcbd96418251ac51a98a59"}, + {file = "bitarray-2.7.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d571056115bbdc18f199a9ee4c2a1b5884f5e63a3c05fe43d2fc7fc67320515"}, + {file = "bitarray-2.7.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2a0313657e6656efca2148cfc91c50fdafca6f811b6c7d0906e6ba57134e560"}, + {file = "bitarray-2.7.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d3b5abb73c45d40d27f9795dac9d6eb1515729c13f93dd67df2be07be6549990"}, + {file = "bitarray-2.7.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7776c070943f45cd8303543a6625cf82f2e000ef9c885d52d7828be099e52f42"}, + {file = "bitarray-2.7.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:057f9c53a34e42deed6e8813a82b9c85924f4728be28e3b9b65144569ac5a387"}, + {file = "bitarray-2.7.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8591ad5768860ad186dc94fd58b2932604a7639b57eefbbff2b4865af3407691"}, + {file = "bitarray-2.7.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bd7f4b2df89bf4e298756c0be0be67fb84d6aa49bda60d46805d43f0e643abd5"}, + {file = "bitarray-2.7.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:433f91c8ab8338662aaa86b0677e6c15c35f8f7b65d4c43d7d1647a8198bc0b0"}, + {file = "bitarray-2.7.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:31e60d8341c3189aa156ca8cb2f6370b29d79cf132e3d091714b0a5a9097eb69"}, + {file = "bitarray-2.7.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ea33ed09157e032f0a7a2627ef87f156e9927697f59b55961439d34bf45af23a"}, + {file = "bitarray-2.7.3-cp36-cp36m-win32.whl", hash = "sha256:302149aaff75939beb8af7f32ac9bf922480033a24fb54f4ebc0c9dc175247c4"}, + {file = "bitarray-2.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:7a8995737fae8de03b31ed83acf4f4326a55b217022009d18be19ff87fc9010e"}, + {file = "bitarray-2.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8b2f31a4cc28aef27355ab896e4b4cc2da2204b2b7adb674d8be7fefa0c93868"}, + {file = "bitarray-2.7.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5df624ee8a4098c3b1149f4817f2a4a0121c4920e1c114af324bc52d6659e2b"}, + {file = "bitarray-2.7.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cb1d60ed709989e34e7158d97fdb077a2f2dfc505998a84161a70f81a6101172"}, + {file = "bitarray-2.7.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:748847e58c45a37f23db1f53a6dc16ae32aa80ee504653d79336830de1a79ed7"}, + {file = "bitarray-2.7.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4b7fdb9772e087174f446655bbc497a1600b5758f279c6d44fcf344c13d5c8a"}, + {file = "bitarray-2.7.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86e9c48ffeddb0f943e87ab65e1e95dccc9b44ef3761af3bf9642973ab7646d2"}, + {file = "bitarray-2.7.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d1f49cc51919d6fa0f7eebd073d2c620b80079aa537d084a7fafb46a35c7a4d"}, + {file = "bitarray-2.7.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b43d56c7c96f5a055f4051be426496db2a616840645d0ab3733d5ceacb2f701b"}, + {file = "bitarray-2.7.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:01f8d02c3eae82c98d4259777cb2f042a0b3989d7dceeb37c643cb94b91d5a42"}, + {file = "bitarray-2.7.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d089b1d0b157c9a484f8f7475eecea813d0dc3818adc5bf352903da14fe88fc3"}, + {file = "bitarray-2.7.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1362e9fb78ca72aa52ec1f1fbd62872801302001b0156ed2a1e707850cd30ffd"}, + {file = "bitarray-2.7.3-cp37-cp37m-win32.whl", hash = "sha256:2cdf5700537e5aa4ec9f4a0b498b8d5b03b9859d503e01ea17a6a134a838aa30"}, + {file = "bitarray-2.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1e1553933f4533040491f4e4499bcbbfcee42c4056f56d7e18010e779daab33d"}, + {file = "bitarray-2.7.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1048a29b3d72b1821a3ae9e8d64e71ed96c53a1a36b1da6db02091a424a8f795"}, + {file = "bitarray-2.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:10dc358fe29d7a4c5be78ab2fb5aa50cb8066babd23e0b5589eb68e26afe58d8"}, + {file = "bitarray-2.7.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8ab6770833976448a9a973bc0df63adedc4c30de4774cec5a9928fc496423ebb"}, + {file = "bitarray-2.7.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abe2f829f6f2d330bccf1bcde2192264ab9a15d6d00e507265f46dc66557014"}, + {file = "bitarray-2.7.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87851a82bdf849e3c40ff6d8af5f734634e17f52a8f7f7e74486c2f8ce717578"}, + {file = "bitarray-2.7.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5fc2512bdf5289a1412c936c65d17881d2b46edb0036c63a8d5605dc8d398a3"}, + {file = "bitarray-2.7.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1289f408a8b5c87cdb4fd7975d4021c6e61209ccb956d0411e72bf43c7f78463"}, + {file = "bitarray-2.7.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ee181cc00aaba38d9812f4df4e7d828105b6dde3b068cd2c43f1d8f395e0046"}, + {file = "bitarray-2.7.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:00e93f70cbcbeabd1e79accf1b6f5b2424cd40556e7877f618549523d0031c98"}, + {file = "bitarray-2.7.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3fb6a952796d16c3a309d866eef56a8f4e5591d112c22446e67d33ecb096b44b"}, + {file = "bitarray-2.7.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:0fe747a134f7f5bc0877eee58090ae7e7f23628eeb459f681ade65719c3f246a"}, + {file = "bitarray-2.7.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:2c1b2c91bf991b5c641faee78dd5a751dff6155ec51c7a6c7f922dc85431898e"}, + {file = "bitarray-2.7.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c3956ae54285ab30d802756144887e30e013f81c9f03e5ffff9daa46d8ca0154"}, + {file = "bitarray-2.7.3-cp38-cp38-win32.whl", hash = "sha256:00a6fc4355bd4e6ead54d05187dc4ea39f0af439b336ae113f0194673ed730ae"}, + {file = "bitarray-2.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:305e6f7441c007f296644ba3899c0306ce9fd7a482dbbc06b6e7b7bd6e0ddabc"}, + {file = "bitarray-2.7.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fe80c23409efb41b86efb5e45f334420a9b5b7828f5b3d08b5ff28f03a024d9e"}, + {file = "bitarray-2.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:16345146b61e93ca20679c83537ccf7245f78b17035f5b1a436fd2b75da04c5e"}, + {file = "bitarray-2.7.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1af9b720a048c69e999094e2310138b7cfca5471a9d2c1dbe4b53dd10e516720"}, + {file = "bitarray-2.7.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:088e6e9ea7f0eaf8b672679a68096dbc0a7a7b7a4ed567860f7362e1588370a6"}, + {file = "bitarray-2.7.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:122cd70ee0de2cc9d94da8b8ebcb7dca12b9f4d3beefb94c11e110e1d87503bb"}, + {file = "bitarray-2.7.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb9a8ee23416bd0cfd457118978bc2f6f02c20b95336db486887f670bf92c2b7"}, + {file = "bitarray-2.7.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a544f99c24b6f658907eb9edf290a9c54f4106738b2ab84cd19dc6013cc3abf"}, + {file = "bitarray-2.7.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:980f6564218f853a9341fb045446539d4153338926ed2fb222e86dc9b2ae9b8f"}, + {file = "bitarray-2.7.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f64abe9301b918d2c352e42198cea0196f3639bc1ad23a4a9d8ae97f66068901"}, + {file = "bitarray-2.7.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:029c724bf38c6616b90b1c423b846b63f8d607ed5a23d270e3862696d88a5392"}, + {file = "bitarray-2.7.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:16cb00911584a6e9ca0f42c305714898120dc6bfbbec90dacedeed4690331a47"}, + {file = "bitarray-2.7.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:699b0134e87c0c4e3b224d879d218c4385a06e6b72df73b4c9c9d549155fb837"}, + {file = "bitarray-2.7.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b508e1bba4ec68fd0ef28505e2dad2f56de7df710c8334c97036705a562cb908"}, + {file = "bitarray-2.7.3-cp39-cp39-win32.whl", hash = "sha256:4b84230624d15868e407ba8b66df54fc69ee6a9e9cb6d51eb264b8f2614596f1"}, + {file = "bitarray-2.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:757a08bf0aed5a650a399f8c66bcba00c210bce34408b6d7b09b4837bee8f4da"}, + {file = "bitarray-2.7.3.tar.gz", hash = "sha256:f71256a32609b036adad932e1228b66a6b4e2cae6be397e588ddc0babd9a78b9"}, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"}, + {file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"}, +] [[package]] -category = "main" -description = "Universal encoding detector for Python 2 and 3" -name = "chardet" +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.6.1" +groups = ["dev"] +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.1.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +groups = ["main", "dev"] +files = [ + {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"}, + {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"}, + {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"}, + {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"}, + {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"}, + {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"}, + {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"}, +] + +[[package]] +name = "ckzg" +version = "2.1.8" +description = "Python bindings for C-KZG-4844" optional = false python-versions = "*" -version = "3.0.4" +groups = ["main"] +files = [ + {file = "ckzg-2.1.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8dc23b189b5e061b5a2fcf609b6dc1f62098dd72cf9116e4721f995eb144b0ee"}, + {file = "ckzg-2.1.8-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:71e0e6d84a268c23c29d546a7ef4720a9d2aea597efe4def75205b6ef436900c"}, + {file = "ckzg-2.1.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ccabbd5aea9cf98ae5301ec8b16119720d93f8f155a567e041e2f16b1fafc87"}, + {file = "ckzg-2.1.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a1dee4ec1d8e6702e6008a4d489638027cd640b5d9e5adca48a8c16aa193c6c7"}, + {file = "ckzg-2.1.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:697f5c4016a9f091bdf7e646be0d71e625b5106a9f78c6ab53e93e5f425aa513"}, + {file = "ckzg-2.1.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:50f6281f5c55240f1be254041e0c06d6348c91e21fccd0ed108151f880b6d087"}, + {file = "ckzg-2.1.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9371ac89c420beb6f6d236ce808c7990174dc21bde61032c39235982e4e65697"}, + {file = "ckzg-2.1.8-cp310-cp310-win_amd64.whl", hash = "sha256:c1f0f8e7a14bb4348f3ecaee92d929df3428be1f706034c961e11239d398bcdb"}, + {file = "ckzg-2.1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:adb4868ec73547d040d8551538dc00ebc3822a758a7fb2b762735e097556c13b"}, + {file = "ckzg-2.1.8-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:5e42a07899e62cf8888bdefe1fa200e3248190953bacc1d872e13825b0b91a7f"}, + {file = "ckzg-2.1.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:369af9ec31248a942f2b249bee9f60dccfe6c8ac944095ad26c8083c9df04186"}, + {file = "ckzg-2.1.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dac8202240347c4af5cee9a3ebc62560ae629c8bac2b1ee313fa57cf1c9e4f0a"}, + {file = "ckzg-2.1.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eff52513b32bc83e3b978cae991289cc932d801b6c08aa002d29e44a4290b2f0"}, + {file = "ckzg-2.1.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:23bef3d98c905152f4ff497a6243b6a14e916deef2d8a1d342175be90f6e6425"}, + {file = "ckzg-2.1.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:40e6e33672e474331519b7d07b8c8e0cbec3d817b6341e00fa728a94141fa8e8"}, + {file = "ckzg-2.1.8-cp311-cp311-win_amd64.whl", hash = "sha256:b84d698c81569381a3dd18a9848f8fca03273ae62045494cee5669a885697d8d"}, + {file = "ckzg-2.1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:98abe138d79886e3e1fbbaf05cdf0702a4351f242ad1a8b4802343c7ba149faa"}, + {file = "ckzg-2.1.8-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:2fe01dad7c968bcdf3c063c5192bc7d7d59f66358afb5c99554e5ce2435a95aa"}, + {file = "ckzg-2.1.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bdb7f51ee3cf8e45451bee8c6dce975fddadfe231174d8de9c27a3aa27741b8"}, + {file = "ckzg-2.1.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10339d23e36b8a0a4e6fda7f6c72d6b2fd4e1506f7b64a661ba8c706ee33f335"}, + {file = "ckzg-2.1.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a4456c9027f2edcb50a2279d6035ca971a511d8b0025e6659ff407b87ad841ba"}, + {file = "ckzg-2.1.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88cbec7e2010f64988c249ebb5679b73ccae1c536f4c130d4708bf7b06a8cd69"}, + {file = "ckzg-2.1.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4d2581df10bdfaec00fec6daefdeaa438e66582364e1d1b705710e9d749fc47"}, + {file = "ckzg-2.1.8-cp312-cp312-win_amd64.whl", hash = "sha256:a30f2b980929e898f0b28aa6bf9ae35e7afd5884e354376ad3744669b7cacf3e"}, + {file = "ckzg-2.1.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:26ed4c4d3acbfdb4bfdf1ab1029219657d4565e1c63d36f2695cdfdb5ec0b569"}, + {file = "ckzg-2.1.8-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:a6899908ca3a41e6d2aa19973b398de101a6d64b5189894077ea09a3f508d3fd"}, + {file = "ckzg-2.1.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3db1ca21685d567eea668925c9f85ebd723db41d24bbecaa2f78d8256e1ba9c6"}, + {file = "ckzg-2.1.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f27d7d16be9debf173369248ff06e97fc45826bfb0a743519b49b38539ab6c7"}, + {file = "ckzg-2.1.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dadf0cd0c3c19611e8a1188a2a10316b88fbae56d806f75a67cbe846b8b7ec86"}, + {file = "ckzg-2.1.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cde13789188b7bac6d5bee308532a7bd60ab5c2feeccdef2f1da41be761c7e04"}, + {file = "ckzg-2.1.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f2737534458547ba8ce89663f833041925b104f7906f17b2338d1b36e6c7c4d"}, + {file = "ckzg-2.1.8-cp313-cp313-win_amd64.whl", hash = "sha256:10b483ad6937878f03d556d120a43d323dcb3891eb83313aa71087b54559594b"}, + {file = "ckzg-2.1.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f3e4a3ae1ff3ec811b6d2aac7a246524f72a750af95fe7a01550cdd68677d6f"}, + {file = "ckzg-2.1.8-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:9afe50a28d8d6f130797f0861d4753f76294d983f1e6ead9c17dfa14f8118ad4"}, + {file = "ckzg-2.1.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da197eef7014997b976ae7bc6e0ad42c02a9faa3a4221d6699e8b777761422f1"}, + {file = "ckzg-2.1.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fef164b9a0c7ed57935bf78bd23e701ba82efcca180e9b29814a94928a2d5880"}, + {file = "ckzg-2.1.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c56f588a57d419ac314880931122796e4331be395368c8887ca3ade5c27f539c"}, + {file = "ckzg-2.1.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:37eecbce59271040dfab736db490a28c0e59f189b404e5820e65531dadf84ddb"}, + {file = "ckzg-2.1.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:acc5d33e42ac852fec08ad1991020a958e2534ac6af346a83f579b553bd0bdfd"}, + {file = "ckzg-2.1.8-cp314-cp314-win_amd64.whl", hash = "sha256:5eb3b5327dd0cbaaa6551e01a42af9780e998640c57236e460830bf9a6e6f9b4"}, + {file = "ckzg-2.1.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dbd7021cdc5616df5902ec04876b9af25d17facc39851b4d6630c9c2b209e30a"}, + {file = "ckzg-2.1.8-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:5ecbcd887fa97988ddfc3ac4d1951367fa4f6bb25a6f72d449550ea4be45b938"}, + {file = "ckzg-2.1.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaeaed98d7be94f7c215373f90cd06536237d6ec08d48d2be74c630e03edf73c"}, + {file = "ckzg-2.1.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1030903bf9989957b73fe30a96516e75a8bc27efb65e1b6e9d3507447bfee06"}, + {file = "ckzg-2.1.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff48587f541b625dd0e471ecc866cf91462100a4429e3c21cf4f099e7dff9150"}, + {file = "ckzg-2.1.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0580e8a1780d44a85c6edaa44e30a6a85565ad26593becb320d7353bc05e8627"}, + {file = "ckzg-2.1.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bc8a259bc3cc321413c3c8eb0789cbe35919f4f4bb32946b0e1a484d4303629c"}, + {file = "ckzg-2.1.8-cp314-cp314t-win_amd64.whl", hash = "sha256:f41377a2a63330df64ae6f7cd806a288b21c52aa346151fd8f0551b9e0742289"}, + {file = "ckzg-2.1.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c4ec8234a72cebe419330e13d580048fec25e63d9526a0a9bf65ed134b6ffa9a"}, + {file = "ckzg-2.1.8-cp39-cp39-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:036b8de43a07340609a3a4445d4da542cb9487c58dc0752c2d419c7be1f3dbdb"}, + {file = "ckzg-2.1.8-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9290e8d128921f1906ffa8e6229a878a9e967ea9937697bf90c894fe523e9d11"}, + {file = "ckzg-2.1.8-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad90fe4e55aea73e3df744b97043c43cd3227725c3f8715fbae07b4eb0acbab0"}, + {file = "ckzg-2.1.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8d511e410956aa2fcde7d19f40cefcc166fa84ef3387b3c96083e3798bd21a44"}, + {file = "ckzg-2.1.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e523457f3a463214e2ae8aa37f8b35d519e1a51d772734d381dd090cf7391b5c"}, + {file = "ckzg-2.1.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:70b67a92373b834e910bc527d538c170757ebc70bc8f99beedbfd64d1913dc80"}, + {file = "ckzg-2.1.8-cp39-cp39-win_amd64.whl", hash = "sha256:d2e709e9701d6085d79cf5d2ed12d373beffae1d93d61b0387055a14818973c0"}, + {file = "ckzg-2.1.8.tar.gz", hash = "sha256:d7bef6b425dca6995457fc59fc5b30211d9b28cbbeee0e7a7bef1372e13f29ca"}, +] [[package]] -category = "dev" -description = "Composable command line interface toolkit" name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "7.1.2" +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] -category = "dev" -description = "Cross-platform colored terminal text." -marker = "sys_platform == \"win32\"" name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.4.3" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] -category = "dev" -description = "Code coverage measurement for Python" name = "coverage" +version = "7.2.2" +description = "Code coverage measurement for Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "5.2" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "coverage-7.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c90e73bdecb7b0d1cea65a08cb41e9d672ac6d7995603d6465ed4914b98b9ad7"}, + {file = "coverage-7.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e2926b8abedf750c2ecf5035c07515770944acf02e1c46ab08f6348d24c5f94d"}, + {file = "coverage-7.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57b77b9099f172804e695a40ebaa374f79e4fb8b92f3e167f66facbf92e8e7f5"}, + {file = "coverage-7.2.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:efe1c0adad110bf0ad7fb59f833880e489a61e39d699d37249bdf42f80590169"}, + {file = "coverage-7.2.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2199988e0bc8325d941b209f4fd1c6fa007024b1442c5576f1a32ca2e48941e6"}, + {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:81f63e0fb74effd5be736cfe07d710307cc0a3ccb8f4741f7f053c057615a137"}, + {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:186e0fc9cf497365036d51d4d2ab76113fb74f729bd25da0975daab2e107fd90"}, + {file = "coverage-7.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:420f94a35e3e00a2b43ad5740f935358e24478354ce41c99407cddd283be00d2"}, + {file = "coverage-7.2.2-cp310-cp310-win32.whl", hash = "sha256:38004671848b5745bb05d4d621526fca30cee164db42a1f185615f39dc997292"}, + {file = "coverage-7.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:0ce383d5f56d0729d2dd40e53fe3afeb8f2237244b0975e1427bfb2cf0d32bab"}, + {file = "coverage-7.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3eb55b7b26389dd4f8ae911ba9bc8c027411163839dea4c8b8be54c4ee9ae10b"}, + {file = "coverage-7.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d2b96123a453a2d7f3995ddb9f28d01fd112319a7a4d5ca99796a7ff43f02af5"}, + {file = "coverage-7.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:299bc75cb2a41e6741b5e470b8c9fb78d931edbd0cd009c58e5c84de57c06731"}, + {file = "coverage-7.2.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e1df45c23d4230e3d56d04414f9057eba501f78db60d4eeecfcb940501b08fd"}, + {file = "coverage-7.2.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:006ed5582e9cbc8115d2e22d6d2144a0725db542f654d9d4fda86793832f873d"}, + {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d683d230b5774816e7d784d7ed8444f2a40e7a450e5720d58af593cb0b94a212"}, + {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8efb48fa743d1c1a65ee8787b5b552681610f06c40a40b7ef94a5b517d885c54"}, + {file = "coverage-7.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c752d5264053a7cf2fe81c9e14f8a4fb261370a7bb344c2a011836a96fb3f57"}, + {file = "coverage-7.2.2-cp311-cp311-win32.whl", hash = "sha256:55272f33da9a5d7cccd3774aeca7a01e500a614eaea2a77091e9be000ecd401d"}, + {file = "coverage-7.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:92ebc1619650409da324d001b3a36f14f63644c7f0a588e331f3b0f67491f512"}, + {file = "coverage-7.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5afdad4cc4cc199fdf3e18088812edcf8f4c5a3c8e6cb69127513ad4cb7471a9"}, + {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0484d9dd1e6f481b24070c87561c8d7151bdd8b044c93ac99faafd01f695c78e"}, + {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d530191aa9c66ab4f190be8ac8cc7cfd8f4f3217da379606f3dd4e3d83feba69"}, + {file = "coverage-7.2.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac0f522c3b6109c4b764ffec71bf04ebc0523e926ca7cbe6c5ac88f84faced0"}, + {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ba279aae162b20444881fc3ed4e4f934c1cf8620f3dab3b531480cf602c76b7f"}, + {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:53d0fd4c17175aded9c633e319360d41a1f3c6e352ba94edcb0fa5167e2bad67"}, + {file = "coverage-7.2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c99cb7c26a3039a8a4ee3ca1efdde471e61b4837108847fb7d5be7789ed8fd9"}, + {file = "coverage-7.2.2-cp37-cp37m-win32.whl", hash = "sha256:5cc0783844c84af2522e3a99b9b761a979a3ef10fb87fc4048d1ee174e18a7d8"}, + {file = "coverage-7.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:817295f06eacdc8623dc4df7d8b49cea65925030d4e1e2a7c7218380c0072c25"}, + {file = "coverage-7.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6146910231ece63facfc5984234ad1b06a36cecc9fd0c028e59ac7c9b18c38c6"}, + {file = "coverage-7.2.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:387fb46cb8e53ba7304d80aadca5dca84a2fbf6fe3faf6951d8cf2d46485d1e5"}, + {file = "coverage-7.2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046936ab032a2810dcaafd39cc4ef6dd295df1a7cbead08fe996d4765fca9fe4"}, + {file = "coverage-7.2.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e627dee428a176ffb13697a2c4318d3f60b2ccdde3acdc9b3f304206ec130ccd"}, + {file = "coverage-7.2.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fa54fb483decc45f94011898727802309a109d89446a3c76387d016057d2c84"}, + {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3668291b50b69a0c1ef9f462c7df2c235da3c4073f49543b01e7eb1dee7dd540"}, + {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7c20b731211261dc9739bbe080c579a1835b0c2d9b274e5fcd903c3a7821cf88"}, + {file = "coverage-7.2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5764e1f7471cb8f64b8cda0554f3d4c4085ae4b417bfeab236799863703e5de2"}, + {file = "coverage-7.2.2-cp38-cp38-win32.whl", hash = "sha256:4f01911c010122f49a3e9bdc730eccc66f9b72bd410a3a9d3cb8448bb50d65d3"}, + {file = "coverage-7.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:c448b5c9e3df5448a362208b8d4b9ed85305528313fca1b479f14f9fe0d873b8"}, + {file = "coverage-7.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfe7085783cda55e53510482fa7b5efc761fad1abe4d653b32710eb548ebdd2d"}, + {file = "coverage-7.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9d22e94e6dc86de981b1b684b342bec5e331401599ce652900ec59db52940005"}, + {file = "coverage-7.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:507e4720791977934bba016101579b8c500fb21c5fa3cd4cf256477331ddd988"}, + {file = "coverage-7.2.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc4803779f0e4b06a2361f666e76f5c2e3715e8e379889d02251ec911befd149"}, + {file = "coverage-7.2.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db8c2c5ace167fd25ab5dd732714c51d4633f58bac21fb0ff63b0349f62755a8"}, + {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f68ee32d7c4164f1e2c8797535a6d0a3733355f5861e0f667e37df2d4b07140"}, + {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d52f0a114b6a58305b11a5cdecd42b2e7f1ec77eb20e2b33969d702feafdd016"}, + {file = "coverage-7.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:797aad79e7b6182cb49c08cc5d2f7aa7b2128133b0926060d0a8889ac43843be"}, + {file = "coverage-7.2.2-cp39-cp39-win32.whl", hash = "sha256:db45eec1dfccdadb179b0f9ca616872c6f700d23945ecc8f21bb105d74b1c5fc"}, + {file = "coverage-7.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:8dbe2647bf58d2c5a6c5bcc685f23b5f371909a5624e9f5cd51436d6a9f6c6ef"}, + {file = "coverage-7.2.2-pp37.pp38.pp39-none-any.whl", hash = "sha256:872d6ce1f5be73f05bea4df498c140b9e7ee5418bfa2cc8204e7f9b817caa968"}, + {file = "coverage-7.2.2.tar.gz", hash = "sha256:36dd42da34fe94ed98c39887b86db9d06777b1c8f860520e21126a75507024f2"}, +] [package.extras] -toml = ["toml"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] -category = "main" -description = "Cython implementation of Toolz: High performance functional utilities" name = "cytoolz" +version = "1.1.0" +description = "Cython implementation of Toolz: High performance functional utilities" optional = false -python-versions = "*" -version = "0.10.1" +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "cytoolz-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:72d7043a88ea5e61ba9d17ea0d1c1eff10f645d7edfcc4e56a31ef78be287644"}, + {file = "cytoolz-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d759e9ed421bacfeb456d47af8d734c057b9912b5f2441f95b27ca35e5efab07"}, + {file = "cytoolz-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fdb5be8fbcc0396141189022724155a4c1c93712ac4aef8c03829af0c2a816d7"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c8c0a513dc89bc05cc72893609118815bced5ef201f1a317b4cc3423b3a0e750"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce94db4f8ebe842c30c0ece42ff5de977c47859088c2c363dede5a68f6906484"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b622d4f54e370c853ded94a668f94fe72c6d70e06ac102f17a2746661c27ab52"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:375a65baa5a5b4ff6a0c5ff17e170cf23312e4c710755771ca966144c24216b5"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c0d51bcdb3203a062a78f66bbe33db5e3123048e24a5f0e1402422d79df8ee2d"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1010869529bb05dc9802b6d776a34ca1b6d48b9deec70ad5e2918ae175be5c2f"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a8f2e83295bdb33f35454d6bafcb7845b03b5881dcaed66ecbd726c7f16772"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0499c5e0a8e688ed367a2e51cc13792ae8f08226c15f7d168589fc44b9b9cada"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87d44e6033d4c5e95a7d39ba59b8e105ba1c29b1ccd1d215f26477cc1d64be39"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a68cef396a7de237f7b97422a6a450dfb111722296ba217ba5b34551832f1f6e"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:06ad4c95b258141f138a93ebfdc1d76ac087afc1a82f1401100a1f44b44ba656"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ada59a4b3c59d4ac7162e0ed08667ffa78abf48e975c8a9f9d5b9bc50720f4fd"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a8957bcaea1ba01327a9b219d2adb84144377684f51444253890dab500ca171f"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6d8cdc299d67eb0f3b9ecdafeeb55eb3b7b7470e2d950ac34b05ed4c7a5572b8"}, + {file = "cytoolz-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d8e08464c5cdea4f6df31e84b11ed6bfd79cedb99fbcbfdc15eb9361a6053c5a"}, + {file = "cytoolz-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e49922a7ed54262d41960bf3b835a7700327bf79cff1e9bfc73d79021132ff8"}, + {file = "cytoolz-1.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:943a662d2e72ffc4438d43ab5a1de8d852237775a423236594a3b3e381b8032c"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dba8e5a8c6e3c789d27b0eb5e7ce5ed7d032a7a9aae17ca4ba5147b871f6e327"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44b31c05addb0889167a720123b3b497b28dd86f8a0aeaf3ae4ffa11e2c85d55"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:653cb18c4fc5d8a8cfce2bce650aabcbe82957cd0536827367d10810566d5294"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:853a5b4806915020c890e1ce70cc056bbc1dd8bc44f2d74d555cccfd7aefba7d"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7b44e9de86bea013fe84fd8c399d6016bbb96c37c5290769e5c99460b9c53e5"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:098d628a801dc142e9740126be5624eb7aef1d732bc7a5719f60a2095547b485"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:779ee4096ed7a82cffab89372ffc339631c285079dbf33dbe7aff1f6174985df"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f2ce18dd99533d077e9712f9faa852f389f560351b1efd2f2bdb193a95eddde2"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac266a34437812cf841cecbfe19f355ab9c3dd1ef231afc60415d40ff12a76e4"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1920b9b9c13d60d0bb6cd14594b3bce0870022eccb430618c37156da5f2b7a55"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47caa376dafd2bdc29f8a250acf59c810ec9105cd6f7680b9a9d070aae8490ec"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ab2c97d8aaa522b038cca9187b1153347af22309e7c998b14750c6fdec7b1cb"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4bce006121b120e8b359244ee140bb0b1093908efc8b739db8dbaa3f8fb42139"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fc0f1e4e9bb384d26e73c6657bbc26abdae4ff66a95933c00f3d578be89181b"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd3f894ff972da1994d06ac6157d74e40dda19eb31fe5e9b7863ca4278c3a167"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0846f49cf8a4496bd42659040e68bd0484ce6af819709cae234938e039203ba0"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:16a3af394ade1973226d64bb2f9eb3336adbdea03ed5b134c1bbec5a3b20028e"}, + {file = "cytoolz-1.1.0-cp311-cp311-win32.whl", hash = "sha256:b786c9c8aeab76cc2f76011e986f7321a23a56d985b77d14f155d5e5514ea781"}, + {file = "cytoolz-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ebf06d1c5344fb22fee71bf664234733e55db72d74988f2ecb7294b05e4db30c"}, + {file = "cytoolz-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b63f5f025fac893393b186e132e3e242de8ee7265d0cd3f5bdd4dda93f6616c9"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:99f8e134c9be11649342853ec8c90837af4089fc8ff1e8f9a024a57d1fa08514"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6f44cf9319c30feb9a50aa513d777ef51efec16f31c404409e7deb8063df64"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:945580dc158c557172fca899a35a99a16fbcebf6db0c77cb6621084bc82189f9"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:257905ec050d04f2f856854620d1e25556fd735064cebd81b460f54939b9f9d5"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82779049f352fb3ab5e8c993ab45edbb6e02efb1f17f0b50f4972c706cc51d76"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7d3e405e435320e08c5a1633afaf285a392e2d9cef35c925d91e2a31dfd7a688"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:923df8f5591e0d20543060c29909c149ab1963a7267037b39eee03a83dbc50a8"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:25db9e4862f22ea0ae2e56c8bec9fc9fd756b655ae13e8c7b5625d7ed1c582d4"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7a98deb11ccd8e5d9f9441ef2ff3352aab52226a2b7d04756caaa53cd612363"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dce4ee9fc99104bc77efdea80f32ca5a650cd653bcc8a1d984a931153d3d9b58"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80d6da158f7d20c15819701bbda1c041f0944ede2f564f5c739b1bc80a9ffb8b"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3b5c5a192abda123ad45ef716ec9082b4cf7d95e9ada8291c5c2cc5558be858b"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5b399ce7d967b1cb6280250818b786be652aa8ddffd3c0bb5c48c6220d945ab5"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e7e29a1a03f00b4322196cfe8e2c38da9a6c8d573566052c586df83aacc5663c"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5291b117d71652a817ec164e7011f18e6a51f8a352cc9a70ed5b976c51102fda"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8caef62f846a9011676c51bda9189ae394cdd6bb17f2946ecaedc23243268320"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:de425c5a8e3be7bb3a195e19191d28d9eb3c2038046064a92edc4505033ec9cb"}, + {file = "cytoolz-1.1.0-cp312-cp312-win32.whl", hash = "sha256:296440a870e8d1f2e1d1edf98f60f1532b9d3ab8dfbd4b25ec08cd76311e79e5"}, + {file = "cytoolz-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:07156987f224c6dac59aa18fb8bf91e1412f5463961862716a3381bf429c8699"}, + {file = "cytoolz-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:23e616b38f5b3160c7bb45b0f84a8f3deb4bd26b29fb2dfc716f241c738e27b8"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:76c9b58555300be6dde87a41faf1f97966d79b9a678b7a526fcff75d28ef4945"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d1d638b10d3144795655e9395566ce35807df09219fd7cacd9e6acbdef67946a"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:26801c1a165e84786a99e03c9c9973356caaca002d66727b761fb1042878ef06"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a9a464542912d3272f6dccc5142df057c71c6a5cbd30439389a732df401afb7"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed6104fa942aa5784bf54f339563de637557e3443b105760bc4de8f16a7fc79b"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56161f0ab60dc4159ec343509abaf809dc88e85c7e420e354442c62e3e7cbb77"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:832bd36cc9123535f1945acf6921f8a2a15acc19cfe4065b1c9b985a28671886"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1842636b6e034f229bf084c2bcdcfd36c8437e752eefd2c74ce9e2f10415cb6e"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:823df012ab90d2f2a0f92fea453528539bf71ac1879e518524cd0c86aa6df7b9"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f1fcf9e7e7b3487883ff3f815abc35b89dcc45c4cf81c72b7ee457aa72d197b"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4cdb3fa1772116827f263f25b0cdd44c663b6701346a56411960534a06c082de"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1b5c95041741b81430454db65183e133976f45ac3c03454cfa8147952568529"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2079fd9f1a65f4c61e6278c8a6d4f85edf30c606df8d5b32f1add88cbbe2286"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a92a320d72bef1c7e2d4c6d875125cf57fc38be45feb3fac1bfa64ea401f54a4"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06d1c79aa51e6a92a90b0e456ebce2288f03dd6a76c7f582bfaa3eda7692e8a5"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e1d7be25f6971e986a52b6d3a0da28e1941850985417c35528f6823aef2cfec5"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:964b248edc31efc50a65e9eaa0c845718503823439d2fa5f8d2c7e974c2b5409"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c9ff2b3c57c79b65cb5be14a18c6fd4a06d5036fb3f33e973a9f70e9ac13ca28"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:22290b73086af600042d99f5ce52a43d4ad9872c382610413176e19fc1d4fd2d"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ade74fccd080ea793382968913ee38d7a35c921df435bbf0a6aeecf0d17574"}, + {file = "cytoolz-1.1.0-cp313-cp313-win32.whl", hash = "sha256:db5dbcfda1c00e937426cbf9bdc63c24ebbc358c3263bfcbc1ab4a88dc52aa8e"}, + {file = "cytoolz-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e2d3fe3b45c3eb7233746f7aca37789be3dceec3e07dcc406d3e045ea0f7bdc"}, + {file = "cytoolz-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:32c559f95ff44a9ebcbd934acaa1e6dc8f3e6ffce4762a79a88528064873d6d5"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9e2cd93b28f667c5870a070ab2b8bb4397470a85c4b204f2454b0ad001cd1ca3"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f494124e141a9361f31d79875fe7ea459a3be2b9dadd90480427c0c52a0943d4"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53a3262bf221f19437ed544bf8c0e1980c81ac8e2a53d87a9bc075dba943d36f"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:47663e57d3f3f124921f38055e86a1022d0844c444ede2e8f090d3bbf80deb65"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5a8755c4104ee4e3d5ba434c543b5f85fdee6a1f1df33d93f518294da793a60"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d96ff3d381423af1b105295f97de86d1db51732c9566eb37378bab6670c5010"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0ec96b3d537cdf47d4e76ded199f7440715f4c71029b45445cff92c1248808c2"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:208e2f2ef90a32b0acbff3303d90d89b13570a228d491d2e622a7883a3c68148"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d416a81bb0bd517558668e49d30a7475b5445f9bbafaab7dcf066f1e9adba36"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f32e94c91ffe49af04835ee713ebd8e005c85ebe83e7e1fdcc00f27164c2d636"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15d0c6405efc040499c46df44056a5c382f551a7624a41cf3e4c84a96b988a15"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:bf069c5381d757debae891401b88b3a346ba3a28ca45ba9251103b282463fad8"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d5cf15892e63411ec1bd67deff0e84317d974e6ab2cdfefdd4a7cea2989df66"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3e3872c21170f8341656f8692f8939e8800dcee6549ad2474d4c817bdefd62cd"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b9ddeff8e8fd65eb1fcefa61018100b2b627e759ea6ad275d2e2a93ffac147bf"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:02feeeda93e1fa3b33414eb57c2b0aefd1db8f558dd33fdfcce664a0f86056e4"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d08154ad45349162b6c37f12d5d1b2e6eef338e657b85e1621e4e6a4a69d64cb"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win32.whl", hash = "sha256:10ae4718a056948d73ca3e1bb9ab1f95f897ec1e362f829b9d37cc29ab566c60"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1bb77bc6197e5cb19784b6a42bb0f8427e81737a630d9d7dda62ed31733f9e6c"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:563dda652c6ff52d215704fbe6b491879b78d7bbbb3a9524ec8e763483cb459f"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d542cee7c7882d2a914a33dec4d3600416fb336734df979473249d4c53d207a1"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31922849b701b0f24bb62e56eb2488dcd3aa6ae3057694bd6b3b7c4c2bc27c2f"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e68308d32afd31943314735c1335e4ab5696110e96b405f6bdb8f2a8dc771a16"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc4bb48b3b866e1867f7c6411a4229e5b44be3989060663713e10efc24c9bd5f"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:456f77207d1445025d7ef262b8370a05492dcb1490cb428b0f3bf1bd744a89b0"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:174ebc71ebb20a9baeffce6ee07ee2cd913754325c93f99d767380d8317930f7"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8b3604fef602bcd53415055a4f68468339192fd17be39e687ae24f476d23d56e"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3604b959a01f64c366e7d10ec7634d5f5cfe10301e27a8f090f6eb3b2a628a18"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6db2127a3c1bc2f59f08010d2ae53a760771a9de2f67423ad8d400e9ba4276e8"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56584745ac647993a016a21bc76399113b7595e312f8d0a1b140c9fcf9b58a27"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db2c4c3a7f7bd7e03bb1a236a125c8feb86c75802f4ecda6ecfaf946610b2930"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48cb8a692111a285d2b9acd16d185428176bfbffa8a7c274308525fccd01dd42"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d2f344ba5eb17dcf38ee37fdde726f69053f54927db8f8a1bed6ac61e5b1890d"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abf76b1c1abd031f098f293b6d90ee08bdaa45f8b5678430e331d991b82684b1"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ddf9a38a5b686091265ff45b53d142e44a538cd6c2e70610d3bc6be094219032"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:946786755274f07bb2be0400f28adb31d7d85a7c7001873c0a8e24a503428fb3"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b8f78b9fed79cf185ad4ddec099abeef45951bdcb416c5835ba05f0a1242c7"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fccde6efefdbc02e676ccb352a2ccc8a8e929f59a1c6d3d60bb78e923a49ca44"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:717b7775313da5f51b0fbf50d865aa9c39cb241bd4cb605df3cf2246d6567397"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5158744a09d0e0e4a4f82225e3a3c4ebf38f9ae74467aaa905467270e52f2794"}, + {file = "cytoolz-1.1.0-cp314-cp314-win32.whl", hash = "sha256:1ed534bdbbf063b2bb28fca7d0f6723a3e5a72b086e7c7fe6d74ae8c3e4d00e2"}, + {file = "cytoolz-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:472c1c9a085f5ad973ec0ad7f0b9ba0969faea6f96c9e397f6293d386f3a25ec"}, + {file = "cytoolz-1.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:a7ad7ca3386fa86bd301be3fa36e7f0acb024f412f665937955acfc8eb42deff"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:64b63ed4b71b1ba813300ad0f06b8aff19a12cf51116e0e4f1ed837cea4debcf"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a60ba6f2ed9eb0003a737e1ee1e9fa2258e749da6477946008d4324efa25149f"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1aa58e2434d732241f7f051e6f17657e969a89971025e24578b5cbc6f1346485"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6965af3fc7214645970e312deb9bd35a213a1eaabcfef4f39115e60bf2f76867"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddd2863f321d67527d3b67a93000a378ad6f967056f68c06467fe011278a6d0e"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4e6b428e9eb5126053c2ae0efa62512ff4b38ed3951f4d0888ca7005d63e56f5"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d758e5ef311d2671e0ae8c214c52e44617cf1e58bef8f022b547b9802a5a7f30"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a95416eca473e6c1179b48d86adcf528b59c63ce78f4cb9934f2e413afa9b56b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36c8ede93525cf11e2cc787b7156e5cecd7340193ef800b816a16f1404a8dc6d"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c949755b6d8a649c5fbc888bc30915926f1b09fe42fea9f289e297c2f6ddd3"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1b6d37545816905a76d9ed59fa4e332f929e879f062a39ea0f6f620405cdc27"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05332112d4087904842b36954cd1d3fc0e463a2f4a7ef9477bd241427c593c3b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:31538ca2fad2d688cbd962ccc3f1da847329e2258a52940f10a2ac0719e526be"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:747562aa70abf219ea16f07d50ac0157db856d447f7f498f592e097cbc77df0b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3dc15c48b20c0f467e15e341e102896c8422dccf8efc6322def5c1b02f074629"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3c03137ee6103ba92d5d6ad6a510e86fded69cd67050bd8a1843f15283be17ac"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be8e298d88f88bd172b59912240558be3b7a04959375646e7fd4996401452941"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:3d407140f5604a89578285d4aac7b18b8eafa055cf776e781aabb89c48738fad"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56e5afb69eb6e1b3ffc34716ee5f92ffbdb5cb003b3a5ca4d4b0fe700e217162"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:27b19b4a286b3ff52040efa42dbe403730aebe5fdfd2def704eb285e2125c63e"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08a63935c66488511b7b29b06233be0be5f4123622fc8fd488f28dc1b7e4c164"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93bd0afcc4cc05794507084afaefb161c3639f283ee629bd0e8654b5c0327ba8"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f3d4da470cfd5cf44f6d682c6eb01363066e0af53ebe111225e44a618f9453d"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba6c12d0e6a67399f4102b4980f4f1bebdbf226ed0a68e84617709d4009b4e71"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b557071405b4aeeaa7cbec1a95d15d6c8f37622fe3f4b595311e0e226ce772c"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cdb406001474726a47fbe903f3aba0de86f5c0b9c9861f55c09c366368225ae0"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6072876ba56446d9ac29d349983677d6f44c6d1c6c1c6be44e66e377c57c767"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c3784c965c9a6822d315d099c3a85b0884ac648952815891c667b469116f1d0"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cc537ad78981df1a827773069fd3b7774f4478db43f518b1616efaf87d7d8f9"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:574ee9dfdc632db8bf9237f27f2a687d1a0b90d29d5e96cab2b21fd2b419c17d"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6594efbaea72dc58b368b53e745ad902c8d8cc41286f00b3743ceac464d5ef3f"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7c849f9ddaf3c7faba938440f9c849235a2908b303063d49da3092a93acd695b"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1fef0296fb3577d0a08ad9b70344ee418f728f1ec21a768ffe774437d67ac859"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1dce1e66fdf72cc474367bd7a7f2b90ec67bb8197dc3fe8ecd08f4ce3ab950a1"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:202fe9975efaec0085cab14a6a6050418bc041f5316f2cf098c0cd2aced4c50e"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:528349434601b9d55e65c6a495494de0001c9a06b431547fea4c60b5edc7d5b3"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3e248cdbf2a54bafdadf4486ddd32e8352f816d3caa2014e44de99f8c525d4a8"}, + {file = "cytoolz-1.1.0-cp39-cp39-win32.whl", hash = "sha256:e63f2b70f4654648a5c6a176ae80897c0de6401f385540dce8e365019e800cfe"}, + {file = "cytoolz-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:f731c53ed29959f105ae622b62e39603c207ed8e8cb2a40cd4accb63d9f92901"}, + {file = "cytoolz-1.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:5a2120bf9e6e8f25e1b32748424a5571e319ef03a995a8fde663fd2feec1a696"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f32e93a55681d782fc6af939f6df36509d65122423cbc930be39b141064adff8"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5d9bc596751cbda8073e65be02ca11706f00029768fbbbc81e11a8c290bb41aa"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b16660d01c3931951fab49db422c627897c38c1a1f0393a97582004019a4887"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b7de5718e2113d4efccea3f06055758cdbc17388ecc3341ba4d1d812837d7c1a"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a12a2a1a6bc44099491c05a12039efa08cc33a3d0f8c7b0566185e085e139283"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:047defa7f5f9a32f82373dbc3957289562e8a3fa58ae02ec8e4dca4f43a33a21"}, + {file = "cytoolz-1.1.0.tar.gz", hash = "sha256:13a7bf254c3c0d28b12e2290b82aed0f0977a4c2a2bf84854fcdc7796a29f3b0"}, +] [package.dependencies] toolz = ">=0.8.0" [package.extras] -cython = ["cython"] +cython = ["cython (>=0.29)"] +test = ["pytest"] + +[[package]] +name = "distlib" +version = "0.4.0" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, + {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, +] + +[[package]] +name = "docutils" +version = "0.22.4" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de"}, + {file = "docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968"}, +] [[package]] -category = "main" -description = "eth_abi: Python utilities for working with Ethereum ABI definitions, especially encoding and decoding" name = "eth-abi" +version = "6.0.0" +description = "eth_abi: Python utilities for working with Ethereum ABI definitions, especially encoding and decoding" optional = false -python-versions = ">=3.6, <4" -version = "2.1.1" +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "eth_abi-6.0.0-py3-none-any.whl", hash = "sha256:05ad63b50bd5448cfac0079b3cc5464de7464b4336a2496bf368d5808c7f9e9f"}, + {file = "eth_abi-6.0.0.tar.gz", hash = "sha256:e83a0ed91f2dadeeb50236d673736fe2edc6fcc0a1c1e13d461192d4b23d5bcc"}, +] [package.dependencies] -eth-typing = ">=2.0.0,<3.0.0" -eth-utils = ">=1.2.0,<2.0.0" -parsimonious = ">=0.8.0,<0.9.0" +eth-typing = ">=3.0.0" +eth-utils = ">=2.0.0" +parsimonious = ">=0.10.0,<0.11.0" [package.extras] -dev = ["bumpversion (>=0.5.3,<1)", "pytest-watch (>=4.1.0,<5)", "wheel", "twine", "ipython", "pytest (4.4.1)", "pytest-pythonpath (>=0.7.1)", "pytest-xdist (1.22.3)", "tox (>=2.9.1,<3)", "eth-hash", "hypothesis (>=3.6.1,<4)", "flake8 (3.4.1)", "isort (>=4.2.15,<5)", "mypy (0.701)", "pydocstyle (>=3.0.0,<4)", "Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)", "towncrier (>=19.2.0,<20)"] -doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)", "towncrier (>=19.2.0,<20)"] -lint = ["flake8 (3.4.1)", "isort (>=4.2.15,<5)", "mypy (0.701)", "pydocstyle (>=3.0.0,<4)"] -test = ["pytest (4.4.1)", "pytest-pythonpath (>=0.7.1)", "pytest-xdist (1.22.3)", "tox (>=2.9.1,<3)", "eth-hash", "hypothesis (>=3.6.1,<4)"] -tools = ["hypothesis (>=3.6.1,<4)"] +tools = ["hypothesis (>=6.22.0,<6.108.7)"] [[package]] -category = "main" -description = "eth-account: Sign Ethereum transactions and messages with local private keys" name = "eth-account" +version = "0.14.0" +description = "eth-account: Sign Ethereum transactions and messages with local private keys" optional = false -python-versions = ">=3.6, <4" -version = "0.5.2" +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "eth_account-0.14.0-py3-none-any.whl", hash = "sha256:efdcb57f32f133e9152510e44772a4bcfe519317dfd8f7e7c5ead8189f73a2b6"}, + {file = "eth_account-0.14.0.tar.gz", hash = "sha256:2c8291b1a8fcbd29a55b07f75f0a0aaffd04704d3ec28d1396587f18a5541c6d"}, +] [package.dependencies] -bitarray = ">=1.2.1,<1.3.0" -eth-abi = ">=2.0.0b7,<3" -eth-keyfile = ">=0.5.0,<0.6.0" -eth-keys = ">=0.2.1,<0.3.2 || >0.3.2,<0.4.0" -eth-rlp = ">=0.1.2,<1" -eth-utils = ">=1.3.0,<2" -hexbytes = ">=0.1.0,<1" -rlp = ">=1.0.0,<2" - -[package.extras] -dev = ["bumpversion (>=0.5.3,<1)", "pytest-watch (>=4.1.0,<5)", "wheel", "twine", "ipython", "hypothesis (>=4.18.0,<5)", "pytest (>=4.4.0,<5)", "pytest-xdist", "tox (>=2.9.1,<3)", "flake8 (3.7.9)", "isort (>=4.2.15,<5)", "mypy (0.701)", "pydocstyle (>=3.0.0,<4)", "Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)", "towncrier (>=19.2.0,<20)"] -doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)", "towncrier (>=19.2.0,<20)"] -lint = ["flake8 (3.7.9)", "isort (>=4.2.15,<5)", "mypy (0.701)", "pydocstyle (>=3.0.0,<4)"] -test = ["hypothesis (>=4.18.0,<5)", "pytest (>=4.4.0,<5)", "pytest-xdist", "tox (>=2.9.1,<3)"] +bitarray = ">=2.4" +ckzg = ">=2" +eth-abi = ">=4" +eth-keyfile = ">=0.10" +eth-keys = ">=0.5" +eth-rlp = ">=3" +eth-utils = ">=6" +hexbytes = ">=2" +pydantic = ">=2" +rlp = ">=2" [[package]] -category = "main" -description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" name = "eth-hash" +version = "0.5.1" +description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" optional = false -python-versions = ">=3.5, <4" -version = "0.2.0" +python-versions = ">=3.7, <4" +groups = ["main"] +files = [ + {file = "eth-hash-0.5.1.tar.gz", hash = "sha256:9805075f653e114a31a99678e93b257fb4082337696f4eff7b4371fe65158409"}, + {file = "eth_hash-0.5.1-py3-none-any.whl", hash = "sha256:4d992e885f3ae3901abbe98bd776ba62d0f6335f98c6e9fc60a39b9d114dfb5a"}, +] [package.dependencies] -[package.dependencies.pycryptodome] -optional = true -version = ">=3.6.6,<4" +pycryptodome = {version = ">=3.6.6,<4", optional = true, markers = "extra == \"pycryptodome\""} [package.extras] -dev = ["bumpversion (>=0.5.3,<1)", "pytest-xdist", "pytest-watch (>=4.1.0,<5)", "wheel", "ipython", "pytest (3.3.2)", "tox (>=2.9.1,<3)", "flake8 (3.4.1)", "isort (>=4.2.15,<5)", "Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"] -doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"] -lint = ["flake8 (3.4.1)", "isort (>=4.2.15,<5)"] +dev = ["Sphinx (>=5.0.0,<6)", "black (>=22.0,<23)", "bumpversion (>=0.5.3,<1)", "flake8 (==3.7.9)", "ipython", "isort (>=4.2.15,<5)", "jinja2 (>=3.0.0,<3.1.0)", "mypy (==0.961)", "pydocstyle (>=5.0.0,<6)", "pytest (>=6.2.5,<7)", "pytest-watch (>=4.1.0,<5)", "pytest-xdist (>=2.4.0,<3)", "sphinx-rtd-theme (>=0.1.9,<1)", "towncrier (>=21,<22)", "tox (>=3.14.6,<4)", "twine", "wheel"] +doc = ["Sphinx (>=5.0.0,<6)", "jinja2 (>=3.0.0,<3.1.0)", "sphinx-rtd-theme (>=0.1.9,<1)", "towncrier (>=21,<22)"] +lint = ["black (>=22.0,<23)", "flake8 (==3.7.9)", "isort (>=4.2.15,<5)", "mypy (==0.961)", "pydocstyle (>=5.0.0,<6)"] pycryptodome = ["pycryptodome (>=3.6.6,<4)"] -pysha3 = ["pysha3 (>=1.0.0,<2.0.0)"] -test = ["pytest (3.3.2)", "tox (>=2.9.1,<3)"] +pysha3 = ["pysha3 (>=1.0.0,<2.0.0) ; python_version < \"3.9\"", "safe-pysha3 (>=1.0.0) ; python_version >= \"3.9\""] +test = ["pytest (>=6.2.5,<7)", "pytest-xdist (>=2.4.0,<3)", "tox (>=3.14.6,<4)"] [[package]] -category = "main" -description = "A library for handling the encrypted keyfiles used to store ethereum private keys." name = "eth-keyfile" +version = "0.10.0" +description = "A library for handling the encrypted keyfiles used to store ethereum private keys" optional = false -python-versions = "*" -version = "0.5.1" +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "eth_keyfile-0.10.0-py3-none-any.whl", hash = "sha256:6b8b1e2528ecd3c53f9f733c061a8ddc7aa40b689f15c2f38b73ee6fc5d274fd"}, + {file = "eth_keyfile-0.10.0.tar.gz", hash = "sha256:3003b20000d68203e8fbf45456851a524f859a7432d2fae73be4a9aebeb4b8e1"}, +] [package.dependencies] -cytoolz = ">=0.9.0,<1.0.0" -eth-keys = ">=0.1.0-beta.4,<1.0.0" -eth-utils = ">=1.0.0-beta.1,<2.0.0" -pycryptodome = ">=3.4.7,<4.0.0" +eth-keys = ">=0.8.0" +eth-utils = ">=2" +py_ecc = ">=5.2.0" +pycryptodome = ">=3.6.6,<4" [[package]] -category = "main" -description = "Common API for Ethereum key operations." name = "eth-keys" +version = "0.8.0" +description = "eth-keys: Common API for Ethereum key operations" optional = false -python-versions = "*" -version = "0.3.3" +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "eth_keys-0.8.0-py3-none-any.whl", hash = "sha256:a7b94222638cccbdf2b5dae5c365d883a96826d82bb0faeb56baa65375f514ae"}, + {file = "eth_keys-0.8.0.tar.gz", hash = "sha256:11549b251876fccd7caedd6905e494ea2309aec352ec2579b00ef9978017a964"}, +] [package.dependencies] -eth-typing = ">=2.2.1,<3.0.0" -eth-utils = ">=1.3.0,<2.0.0" +eth-typing = ">=3" +eth-utils = ">=2" [package.extras] -coincurve = ["coincurve (>=7.0.0,<13.0.0)"] -dev = ["tox (2.7.0)", "bumpversion (0.5.3)", "twine", "eth-utils (>=1.3.0,<2.0.0)", "eth-typing (>=2.2.1,<3.0.0)", "flake8 (3.0.4)", "mypy (0.701)", "asn1tools (>=0.146.2,<0.147)", "pyasn1 (>=0.4.5,<0.5)", "pytest (3.2.2)", "hypothesis (>=4.56.1,<5.0.0)", "eth-hash", "eth-hash"] -eth-keys = ["eth-utils (>=1.3.0,<2.0.0)", "eth-typing (>=2.2.1,<3.0.0)"] -lint = ["flake8 (3.0.4)", "mypy (0.701)"] -test = ["asn1tools (>=0.146.2,<0.147)", "pyasn1 (>=0.4.5,<0.5)", "pytest (3.2.2)", "hypothesis (>=4.56.1,<5.0.0)", "eth-hash", "eth-hash"] +coincurve = ["coincurve (>=21.0.0) ; python_version < \"3.14\""] [[package]] -category = "main" -description = "eth-rlp: RLP definitions for common Ethereum objects in Python" name = "eth-rlp" +version = "3.0.0" +description = "eth-rlp: RLP definitions for common Ethereum objects in Python" optional = false -python-versions = ">=3.5, <4" -version = "0.1.2" +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "eth_rlp-3.0.0-py3-none-any.whl", hash = "sha256:32f355261c36ad2c369db098170f873123795667e50d496b034f369e2c9d2346"}, + {file = "eth_rlp-3.0.0.tar.gz", hash = "sha256:9663e54a4a1c1c847d2d328c1d07e4174ec1c082953fbb42b60e61c501c4931c"}, +] [package.dependencies] -eth-utils = ">=1.0.1,<2" -hexbytes = ">=0.1.0,<1" -rlp = ">=0.6.0,<2" - -[package.extras] -dev = ["bumpversion (>=0.5.3,<1)", "pytest-xdist", "pytest-watch (>=4.1.0,<5)", "wheel", "ipython", "pytest (3.3.2)", "tox (>=2.9.1,<3)", "eth-hash", "flake8 (3.4.1)", "isort (>=4.2.15,<5)", "Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"] -doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"] -lint = ["flake8 (3.4.1)", "isort (>=4.2.15,<5)"] -test = ["pytest (3.3.2)", "tox (>=2.9.1,<3)", "eth-hash"] +eth-utils = ">=2.0.0" +hexbytes = ">=1.2.0" +rlp = ">=3.0.0" [[package]] -category = "main" -description = "eth-typing: Common type annotations for ethereum python packages" name = "eth-typing" +version = "6.0.0" +description = "eth-typing: Common type annotations for ethereum python packages" optional = false -python-versions = ">=3.5, <4" -version = "2.2.1" - -[package.extras] -dev = ["bumpversion (>=0.5.3,<1)", "pytest-watch (>=4.1.0,<5)", "wheel", "twine", "ipython", "pytest (>=4.4,<4.5)", "pytest-xdist", "tox (>=2.9.1,<3)", "flake8 (3.4.1)", "isort (>=4.2.15,<5)", "mypy (0.701)", "pydocstyle (>=3.0.0,<4)", "Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"] -doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"] -lint = ["flake8 (3.4.1)", "isort (>=4.2.15,<5)", "mypy (0.701)", "pydocstyle (>=3.0.0,<4)"] -test = ["pytest (>=4.4,<4.5)", "pytest-xdist", "tox (>=2.9.1,<3)"] - -[[package]] -category = "main" -description = "Common utility functions for ethereum codebases." -name = "eth-utils" -optional = false -python-versions = ">=3.5,!=3.5.2,<4" -version = "1.9.0" +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "eth_typing-6.0.0-py3-none-any.whl", hash = "sha256:ee74fb641eb36dd885e1c42c2a3055314efa532b3e71480816df70a94d35cfb9"}, + {file = "eth_typing-6.0.0.tar.gz", hash = "sha256:315dd460dc0b71c15a6cd51e3c0b70d237eec8771beb844144f3a1fb4adb2392"}, +] [package.dependencies] -cytoolz = ">=0.10.1,<1.0.0" -eth-hash = ">=0.1.0,<1.0.0" -eth-typing = ">=2.2.1,<3.0.0" -toolz = ">0.8.2,<1" +typing_extensions = ">=4.5.0" [package.extras] -deploy = ["bumpversion (>=0.5.3,<1.0.0)", "tox (>=2.9.1,<3.0.0)", "wheel (>=0.30.0,<1.0.0)"] -dev = ["twine (>=1.13,<2)", "Sphinx (>=1.5.5,<2)", "sphinx-rtd-theme (>=0.1.9,<2)", "towncrier (>=19.2.0,<20)", "black (>=18.6b4,<19)", "flake8 (>=3.7.0,<4.0.0)", "isort (4.3.18)", "mypy (0.701)", "pytest (>=3.4.1,<4.0.0)", "hypothesis (>=4.43.0,<5.0.0)", "pytest-pythonpath (>=0.3,<1.0)", "bumpversion (>=0.5.3,<1.0.0)", "tox (>=2.9.1,<3.0.0)", "wheel (>=0.30.0,<1.0.0)"] -doc = ["Sphinx (>=1.5.5,<2)", "sphinx-rtd-theme (>=0.1.9,<2)", "towncrier (>=19.2.0,<20)"] -lint = ["black (>=18.6b4,<19)", "flake8 (>=3.7.0,<4.0.0)", "isort (4.3.18)", "mypy (0.701)", "pytest (>=3.4.1,<4.0.0)"] -test = ["hypothesis (>=4.43.0,<5.0.0)", "pytest (>=3.4.1,<4.0.0)", "pytest-pythonpath (>=0.3,<1.0)"] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.18.2)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel (>=0.38.1)"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] -category = "dev" -description = "the modular source code checker: pep8 pyflakes and co" -name = "flake8" +name = "eth-utils" +version = "6.0.0" +description = "eth-utils: Common utility functions for python code that interacts with Ethereum" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "3.8.3" +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "eth_utils-6.0.0-py3-none-any.whl", hash = "sha256:63cf48ee32c45541cb5748751909a8345c470432fb6f0fed4bd7c53fd6400469"}, + {file = "eth_utils-6.0.0.tar.gz", hash = "sha256:eb54b2f82dd300d3142c49a89da195e823f5e5284d43203593f87c67bad92a96"}, +] [package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.6.0a1,<2.7.0" -pyflakes = ">=2.2.0,<2.3.0" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" - -[[package]] -category = "main" -description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" -name = "hexbytes" -optional = false -python-versions = ">=3.6, <4" -version = "0.2.1" +cytoolz = {version = ">=0.10.1", markers = "implementation_name == \"cpython\""} +eth-hash = ">=0.3.1" +eth-typing = ">=5.0.0" +pydantic = ">=2.0.0,<3" +toolz = {version = ">0.8.2", markers = "implementation_name == \"pypy\""} [package.extras] -dev = ["bumpversion (>=0.5.3,<1)", "pytest-watch (>=4.1.0,<5)", "wheel", "twine", "ipython", "pytest (5.4.1)", "pytest-xdist", "tox (3.14.6)", "hypothesis (>=3.44.24,<4)", "eth-utils (>=1.0.1,<2)", "flake8 (3.7.9)", "isort (>=4.2.15,<5)", "mypy (0.770)", "pydocstyle (>=3.0.0,<4)", "Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)", "towncrier (>=19.2.0,<20)"] -doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)", "towncrier (>=19.2.0,<20)"] -lint = ["flake8 (3.7.9)", "isort (>=4.2.15,<5)", "mypy (0.770)", "pydocstyle (>=3.0.0,<4)"] -test = ["pytest (5.4.1)", "pytest-xdist", "tox (3.14.6)", "hypothesis (>=3.44.24,<4)", "eth-utils (>=1.0.1,<2)"] - -[[package]] -category = "main" -description = "Internationalized Domain Names in Applications (IDNA)" -name = "idna" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.10" +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=4.43.0)", "ipython", "mypy (==1.18.2)", "mypy (==1.18.2)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel (>=0.38.1)"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["hypothesis (>=4.43.0)", "mypy (==1.18.2)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] -category = "main" -description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" -name = "importlib-metadata" +name = "filelock" +version = "3.12.2" +description = "A platform independent file lock." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.7.0" - -[package.dependencies] -zipp = ">=0.5" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"}, + {file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"}, +] [package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] +docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] + +[[package]] +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, +] [[package]] -category = "main" -description = "Python IPFS HTTP CLIENT library" -name = "ipfshttpclient" +name = "hexbytes" +version = "2.0.0" +description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" optional = false -python-versions = ">=3.5" -version = "0.6.0.post1" - -[package.dependencies] -multiaddr = ">=0.0.7" -requests = ">=2.11" +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "hexbytes-2.0.0-py3-none-any.whl", hash = "sha256:5425bd7ac83cdd9791c13a5bf97cfe9b9609a304b1ef3ab146adfd50de06cf0e"}, + {file = "hexbytes-2.0.0.tar.gz", hash = "sha256:01312fcd5c57e8a8d2d7dd3274dcf84ea50422aff2abcc2d9fd89ad6a32498e5"}, +] [[package]] -category = "main" -description = "An implementation of JSON Schema validation for Python" -name = "jsonschema" +name = "identify" +version = "2.5.24" +description = "File identification library for Python" optional = false -python-versions = "*" -version = "3.2.0" - -[package.dependencies] -attrs = ">=17.4.0" -pyrsistent = ">=0.14.0" -setuptools = "*" -six = ">=1.11.0" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = "*" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "identify-2.5.24-py2.py3-none-any.whl", hash = "sha256:986dbfb38b1140e763e413e6feb44cd731faf72d1909543178aa79b0e258265d"}, + {file = "identify-2.5.24.tar.gz", hash = "sha256:0aac67d5b4812498056d28a9a512a483f5085cc28640b02b258a59dac34301d4"}, +] [package.extras] -format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] -format_nongpl = ["idna", "jsonpointer (>1.13)", "webcolors", "rfc3986-validator (>0.1.0)", "rfc3339-validator"] +license = ["ukkonen"] [[package]] -category = "main" -description = "An Dict like LRU container." -name = "lru-dict" +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = "*" -version = "1.1.6" +python-versions = ">=3.5" +groups = ["main", "dev"] +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] [[package]] -category = "dev" -description = "McCabe checker, plugin for flake8" -name = "mccabe" +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false -python-versions = "*" -version = "0.6.1" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] [[package]] -category = "dev" -description = "More routines for operating on iterables, beyond itertools" -name = "more-itertools" +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.5" -version = "8.4.0" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] [[package]] -category = "main" -description = "Python implementation of jbenet's multiaddr" -name = "multiaddr" +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" -version = "0.0.9" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] [package.dependencies] -base58 = "*" -netaddr = "*" -six = "*" -varint = "*" +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] + +[[package]] +name = "multidict" +version = "6.7.1" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, +] [[package]] -category = "dev" -description = "Optional static typing for Python" name = "mypy" +version = "1.1.1" +description = "Optional static typing for Python" optional = false -python-versions = ">=3.5" -version = "0.782" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "mypy-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39c7119335be05630611ee798cc982623b9e8f0cff04a0b48dfc26100e0b97af"}, + {file = "mypy-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61bf08362e93b6b12fad3eab68c4ea903a077b87c90ac06c11e3d7a09b56b9c1"}, + {file = "mypy-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbb19c9f662e41e474e0cff502b7064a7edc6764f5262b6cd91d698163196799"}, + {file = "mypy-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:315ac73cc1cce4771c27d426b7ea558fb4e2836f89cb0296cbe056894e3a1f78"}, + {file = "mypy-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5cb14ff9919b7df3538590fc4d4c49a0f84392237cbf5f7a816b4161c061829e"}, + {file = "mypy-1.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:26cdd6a22b9b40b2fd71881a8a4f34b4d7914c679f154f43385ca878a8297389"}, + {file = "mypy-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b5f81b40d94c785f288948c16e1f2da37203c6006546c5d947aab6f90aefef2"}, + {file = "mypy-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b437be1c02712a605591e1ed1d858aba681757a1e55fe678a15c2244cd68a5"}, + {file = "mypy-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d809f88734f44a0d44959d795b1e6f64b2bbe0ea4d9cc4776aa588bb4229fc1c"}, + {file = "mypy-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:a380c041db500e1410bb5b16b3c1c35e61e773a5c3517926b81dfdab7582be54"}, + {file = "mypy-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b7c7b708fe9a871a96626d61912e3f4ddd365bf7f39128362bc50cbd74a634d5"}, + {file = "mypy-1.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1c10fa12df1232c936830839e2e935d090fc9ee315744ac33b8a32216b93707"}, + {file = "mypy-1.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0a28a76785bf57655a8ea5eb0540a15b0e781c807b5aa798bd463779988fa1d5"}, + {file = "mypy-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:ef6a01e563ec6a4940784c574d33f6ac1943864634517984471642908b30b6f7"}, + {file = "mypy-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d64c28e03ce40d5303450f547e07418c64c241669ab20610f273c9e6290b4b0b"}, + {file = "mypy-1.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64cc3afb3e9e71a79d06e3ed24bb508a6d66f782aff7e56f628bf35ba2e0ba51"}, + {file = "mypy-1.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce61663faf7a8e5ec6f456857bfbcec2901fbdb3ad958b778403f63b9e606a1b"}, + {file = "mypy-1.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b0c373d071593deefbcdd87ec8db91ea13bd8f1328d44947e88beae21e8d5e9"}, + {file = "mypy-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:2888ce4fe5aae5a673386fa232473014056967f3904f5abfcf6367b5af1f612a"}, + {file = "mypy-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:19ba15f9627a5723e522d007fe708007bae52b93faab00f95d72f03e1afa9598"}, + {file = "mypy-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:59bbd71e5c58eed2e992ce6523180e03c221dcd92b52f0e792f291d67b15a71c"}, + {file = "mypy-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9401e33814cec6aec8c03a9548e9385e0e228fc1b8b0a37b9ea21038e64cdd8a"}, + {file = "mypy-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b398d8b1f4fba0e3c6463e02f8ad3346f71956b92287af22c9b12c3ec965a9f"}, + {file = "mypy-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:69b35d1dcb5707382810765ed34da9db47e7f95b3528334a3c999b0c90fe523f"}, + {file = "mypy-1.1.1-py3-none-any.whl", hash = "sha256:4e4e8b362cdf99ba00c2b218036002bdcdf1e0de085cdb296a49df03fb31dfc4"}, + {file = "mypy-1.1.1.tar.gz", hash = "sha256:ae9ceae0f5b9059f33dbc62dea087e942c0ccab4b7a003719cb70f9b8abfa32f"}, +] [package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -typed-ast = ">=1.4.0,<1.5.0" -typing-extensions = ">=3.7.4" +mypy-extensions = ">=1.0.0" +typing-extensions = ">=3.10" [package.extras] dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +python2 = ["typed-ast (>=1.4.0,<2)"] +reports = ["lxml"] [[package]] -category = "dev" -description = "Experimental type system extensions for programs checked with the mypy typechecker." name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." optional = false -python-versions = "*" -version = "0.4.3" +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] [[package]] -category = "main" -description = "A network address manipulation library for Python" -name = "netaddr" +name = "nodeenv" +version = "1.10.0" +description = "Node.js virtual environment builder" optional = false -python-versions = "*" -version = "0.8.0" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, + {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, +] [[package]] -category = "dev" -description = "Core utilities for Python packages" name = "packaging" +version = "23.0" +description = "Core utilities for Python packages" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "20.4" - -[package.dependencies] -pyparsing = ">=2.0.2" -six = "*" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] [[package]] -category = "main" -description = "(Soon to be) the fastest pure-Python PEG parser I could muster" name = "parsimonious" +version = "0.10.0" +description = "(Soon to be) the fastest pure-Python PEG parser I could muster" optional = false python-versions = "*" -version = "0.8.1" +groups = ["main"] +files = [ + {file = "parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f"}, + {file = "parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c"}, +] [package.dependencies] -six = ">=1.9.0" +regex = ">=2022.3.15" [[package]] -category = "dev" -description = "Utility library for gitignore style pattern matching of file paths." -name = "pathspec" +name = "platformdirs" +version = "3.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "0.8.0" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, + {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] -category = "dev" -description = "plugin and hook calling mechanisms for python" name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.13.1" - -[package.dependencies] -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] [package.extras] dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] -category = "main" -description = "Protocol Buffers" -name = "protobuf" +name = "pre-commit" +version = "2.21.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false -python-versions = "*" -version = "3.12.2" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] [package.dependencies] -setuptools = "*" -six = ">=1.9" +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" [[package]] -category = "dev" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -name = "py" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.9.0" +name = "propcache" +version = "0.5.2" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a81be28596d6559f6131ef33e10200de6e17643b3c74ce03f9eb103be6ae8b"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29cbaac5ea0212663e6845e04b5e188d5a6ae6dd919810ac835bf1d3b42c3f4c"}, + {file = "propcache-0.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6bf3be92233808fcd338eba0fb4d0b59ec5772af4f4ecfcec450d1bfc0f8b5eb"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f8ea531c794b9d6274acd4e8d2c2ebcac590a4361d27482edd3010b79f1325e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:decfca4c79dd53ebab484b00cc4b6717d8c369f86e74aa4ca395a64ac651495e"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4621064bbf28fa77ff64dd5d94367c04684c67d3a5bf1dff25f0cd0d98a38f3b"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b96db7141a592cbc968daf1feea83a118e6ab378af4abbc72b248c895414c22d"}, + {file = "propcache-0.5.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ca071adabaab6e9219924bbe00af821f1ee7de113a9eca1cdc292de3d120f4d"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e4294d04a94dcab1b3bccd8b66d962dcad411a1d19414b2a41d1445f1de32ad0"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a0e399a2eccb91ed18721f86aa85757727400b6865c89e88934781deb9c8498b"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:823581fd5cb08b12a48bfa11fe962a7916766b6170c17b028fbdf762b85eb9bf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:949c91d1a990cf3b2e8188dfcfb25005e0b834a06c63fa4ef9f360878ce21ecf"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:cc1177027eda740fdb152706bd215a3f124e3eea15afc39f2cb9fe351b50619e"}, + {file = "propcache-0.5.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b05d643f944a8c3c4bd86d65ffd87bf3264b617f87791940302bc474d2ff5274"}, + {file = "propcache-0.5.2-cp310-cp310-win32.whl", hash = "sha256:8114f28879e0904748e831c3a7774261bd9e75f49be089f389a76f959dcd13fe"}, + {file = "propcache-0.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:5fcb98e7598b1ee0addab320d90f65b530297a867dbfe9de52ea838077e16e3d"}, + {file = "propcache-0.5.2-cp310-cp310-win_arm64.whl", hash = "sha256:04dc2390d9edbbaef7461f33322555976ffddf0b650a038649d026358714e6c5"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959"}, + {file = "propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b"}, + {file = "propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27"}, + {file = "propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f"}, + {file = "propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0"}, + {file = "propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82"}, + {file = "propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a"}, + {file = "propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476"}, + {file = "propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33"}, + {file = "propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a"}, + {file = "propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031"}, + {file = "propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42"}, + {file = "propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117"}, + {file = "propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d"}, + {file = "propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704"}, + {file = "propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4"}, + {file = "propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d"}, + {file = "propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757"}, + {file = "propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa"}, + {file = "propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc"}, + {file = "propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55"}, + {file = "propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568"}, + {file = "propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191"}, + {file = "propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7"}, + {file = "propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e"}, + {file = "propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825"}, + {file = "propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5"}, + {file = "propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4"}, + {file = "propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0"}, + {file = "propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c"}, + {file = "propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078"}, + {file = "propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335"}, + {file = "propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d"}, + {file = "propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2"}, + {file = "propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821"}, + {file = "propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370"}, + {file = "propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6"}, + {file = "propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe"}, + {file = "propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427"}, +] + +[[package]] +name = "py-ecc" +version = "8.0.0" +description = "py-ecc: Elliptic curve crypto in python including secp256k1, alt_bn128, and bls12_381" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "py_ecc-8.0.0-py3-none-any.whl", hash = "sha256:c0b2dfc4bde67a55122a392591a10e851a986d5128f680628c80b405f7663e13"}, + {file = "py_ecc-8.0.0.tar.gz", hash = "sha256:56aca19e5dc37294f60c1cc76666c03c2276e7666412b9a559fa0145d099933d"}, +] -[[package]] -category = "dev" -description = "Python style guide checker" -name = "pycodestyle" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.6.0" +[package.dependencies] +eth-typing = ">=3.0.0" +eth-utils = ">=2.0.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] [[package]] -category = "main" -description = "Cryptographic library for Python" name = "pycryptodome" +version = "3.17" +description = "Cryptographic library for Python" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "3.9.8" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] +files = [ + {file = "pycryptodome-3.17-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:2c5631204ebcc7ae33d11c43037b2dafe25e2ab9c1de6448eb6502ac69c19a56"}, + {file = "pycryptodome-3.17-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:04779cc588ad8f13c80a060b0b1c9d1c203d051d8a43879117fe6b8aaf1cd3fa"}, + {file = "pycryptodome-3.17-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f812d58c5af06d939b2baccdda614a3ffd80531a26e5faca2c9f8b1770b2b7af"}, + {file = "pycryptodome-3.17-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:9453b4e21e752df8737fdffac619e93c9f0ec55ead9a45df782055eb95ef37d9"}, + {file = "pycryptodome-3.17-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:121d61663267f73692e8bde5ec0d23c9146465a0d75cad75c34f75c752527b01"}, + {file = "pycryptodome-3.17-cp27-cp27m-win32.whl", hash = "sha256:ba2d4fcb844c6ba5df4bbfee9352ad5352c5ae939ac450e06cdceff653280450"}, + {file = "pycryptodome-3.17-cp27-cp27m-win_amd64.whl", hash = "sha256:87e2ca3aa557781447428c4b6c8c937f10ff215202ab40ece5c13a82555c10d6"}, + {file = "pycryptodome-3.17-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f44c0d28716d950135ff21505f2c764498eda9d8806b7c78764165848aa419bc"}, + {file = "pycryptodome-3.17-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5a790bc045003d89d42e3b9cb3cc938c8561a57a88aaa5691512e8540d1ae79c"}, + {file = "pycryptodome-3.17-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:d086d46774e27b280e4cece8ab3d87299cf0d39063f00f1e9290d096adc5662a"}, + {file = "pycryptodome-3.17-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5587803d5b66dfd99e7caa31ed91fba0fdee3661c5d93684028ad6653fce725f"}, + {file = "pycryptodome-3.17-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:e7debd9c439e7b84f53be3cf4ba8b75b3d0b6e6015212355d6daf44ac672e210"}, + {file = "pycryptodome-3.17-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ca1ceb6303be1282148f04ac21cebeebdb4152590842159877778f9cf1634f09"}, + {file = "pycryptodome-3.17-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:dc22cc00f804485a3c2a7e2010d9f14a705555f67020eb083e833cabd5bd82e4"}, + {file = "pycryptodome-3.17-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80ea8333b6a5f2d9e856ff2293dba2e3e661197f90bf0f4d5a82a0a6bc83a626"}, + {file = "pycryptodome-3.17-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c133f6721fba313722a018392a91e3c69d3706ae723484841752559e71d69dc6"}, + {file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:333306eaea01fde50a73c4619e25631e56c4c61bd0fb0a2346479e67e3d3a820"}, + {file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:1a30f51b990994491cec2d7d237924e5b6bd0d445da9337d77de384ad7f254f9"}, + {file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:909e36a43fe4a8a3163e9c7fc103867825d14a2ecb852a63d3905250b308a4e5"}, + {file = "pycryptodome-3.17-cp35-abi3-win32.whl", hash = "sha256:a3228728a3808bc9f18c1797ec1179a0efb5068c817b2ffcf6bcd012494dffb2"}, + {file = "pycryptodome-3.17-cp35-abi3-win_amd64.whl", hash = "sha256:9ec565e89a6b400eca814f28d78a9ef3f15aea1df74d95b28b7720739b28f37f"}, + {file = "pycryptodome-3.17-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:e1819b67bcf6ca48341e9b03c2e45b1c891fa8eb1a8458482d14c2805c9616f2"}, + {file = "pycryptodome-3.17-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:f8e550caf52472ae9126953415e4fc554ab53049a5691c45b8816895c632e4d7"}, + {file = "pycryptodome-3.17-pp27-pypy_73-win32.whl", hash = "sha256:afbcdb0eda20a0e1d44e3a1ad6d4ec3c959210f4b48cabc0e387a282f4c7deb8"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a74f45aee8c5cc4d533e585e0e596e9f78521e1543a302870a27b0ae2106381e"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38bbd6717eac084408b4094174c0805bdbaba1f57fc250fd0309ae5ec9ed7e09"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f68d6c8ea2974a571cacb7014dbaada21063a0375318d88ac1f9300bc81e93c3"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8198f2b04c39d817b206ebe0db25a6653bb5f463c2319d6f6d9a80d012ac1e37"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a232474cd89d3f51e4295abe248a8b95d0332d153bf46444e415409070aae1e"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4992ec965606054e8326e83db1c8654f0549cdb26fce1898dc1a20bc7684ec1c"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53068e33c74f3b93a8158dacaa5d0f82d254a81b1002e0cd342be89fcb3433eb"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:74794a2e2896cd0cf56fdc9db61ef755fa812b4a4900fa46c49045663a92b8d0"}, + {file = "pycryptodome-3.17.tar.gz", hash = "sha256:bce2e2d8e82fcf972005652371a3e8731956a0c1fbb719cc897943b3695ad91b"}, +] + +[[package]] +name = "pydantic" +version = "2.13.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73"}, + {file = "pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08"}, +] -[[package]] -category = "dev" -description = "passive checker of Python programs" -name = "pyflakes" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "2.2.0" +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.46.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win32.whl", hash = "sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win_amd64.whl", hash = "sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266"}, + {file = "pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" [[package]] -category = "dev" -description = "Python parsing module" -name = "pyparsing" +name = "pydata-sphinx-theme" +version = "0.20.0" +description = "Bootstrap-based Sphinx theme from the PyData community" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -version = "2.4.7" +python-versions = ">=3.11" +groups = ["dev"] +files = [ + {file = "pydata_sphinx_theme-0.20.0-py3-none-any.whl", hash = "sha256:56744483c9d72c783e075de716ab95d486108b69605df7528078090b73f11f69"}, + {file = "pydata_sphinx_theme-0.20.0.tar.gz", hash = "sha256:0da172d41e19a66de875f4002f7054b385372ec65763852193791e658d50bb4a"}, +] + +[package.dependencies] +accessible-pygments = "*" +Babel = "*" +beautifulsoup4 = "*" +docutils = "!=0.17.0" +jinja2 = "*" +pygments = ">=2.7" +requests = "*" +sphinx = ">=8.2,<10" + +[package.extras] +a11y = ["pydata-sphinx-theme[test]"] +dev = ["pandoc", "pre-commit", "pydata-sphinx-theme[doc,test]", "pyyaml", "sphinx-theme-builder[cli]", "tox"] +doc = ["ablog (>=0.11.8)", "astroid (>=3,!=4.0.3)", "colorama", "graphviz", "ipykernel", "ipyleaflet", "ipywidgets", "jupyter_sphinx", "jupyterlite-sphinx", "linkify-it-py", "matplotlib", "myst-parser", "nbsphinx", "numpy", "numpydoc", "pandas", "plotly", "rich", "sphinx-autoapi (==3.6.1)", "sphinx-copybutton", "sphinx-design", "sphinx-favicon (>=1.0.1)", "sphinx-sitemap (<2.7.0)", "sphinx-togglebutton", "sphinxcontrib-mermaid", "sphinxcontrib-youtube (>=1.4.1)", "sphinxext-rediraffe", "xarray"] +i18n = ["Babel", "jinja2"] +test = ["pytest", "pytest-cov", "pytest-playwright", "pytest-regressions", "sphinx[test]", "typing-extensions"] [[package]] -category = "main" -description = "" -marker = "platform_system == \"Windows\"" -name = "pypiwin32" +name = "pygments" +version = "2.20.0" +description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = "*" -version = "223" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"}, + {file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"}, +] -[package.dependencies] -pywin32 = ">=223" +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] [[package]] -category = "main" -description = "Persistent/Functional/Immutable data structures" -name = "pyrsistent" +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" optional = false -python-versions = "*" -version = "0.16.0" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] [package.dependencies] -six = "*" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] -category = "dev" -description = "pytest: simple powerful testing with Python" -name = "pytest" +name = "pytest-asyncio" +version = "1.4.0" +description = "Pytest support for asyncio" optional = false -python-versions = ">=3.5" -version = "5.4.3" +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"}, + {file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"}, +] [package.dependencies] -atomicwrites = ">=1.0" -attrs = ">=17.4.0" -colorama = "*" -more-itertools = ">=4.0.0" -packaging = "*" -pluggy = ">=0.12,<1.0" -py = ">=1.5.0" -wcwidth = "*" - -[package.dependencies.importlib-metadata] -python = "<3.8" -version = ">=0.12" +pytest = ">=8.4,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} [package.extras] -checkqa-mypy = ["mypy (v0.761)"] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] -category = "dev" -description = "Pytest plugin for measuring coverage." name = "pytest-cov" +version = "4.0.0" +description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.10.0" +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, +] [package.dependencies] -coverage = ">=4.4" +coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "pytest-xdist", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] -category = "dev" -description = "A py.test plugin that parses environment files before running tests" name = "pytest-dotenv" +version = "0.5.2" +description = "A py.test plugin that parses environment files before running tests" optional = false python-versions = "*" -version = "0.5.2" +groups = ["dev"] +files = [ + {file = "pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732"}, + {file = "pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f"}, +] [package.dependencies] pytest = ">=5.0.0" python-dotenv = ">=0.9.1" [[package]] -category = "dev" -description = "Add .env support to your django/flask apps in development and deployments" name = "python-dotenv" +version = "0.21.1" +description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = "*" -version = "0.14.0" +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "python-dotenv-0.21.1.tar.gz", hash = "sha256:1c93de8f636cde3ce377292818d0e440b6e45a82f215c3744979151fa8151c49"}, + {file = "python_dotenv-0.21.1-py3-none-any.whl", hash = "sha256:41e12e0318bebc859fcc4d97d4db8d20ad21721a6aa5047dd59f090391cb549a"}, +] [package.extras] cli = ["click (>=5.0)"] [[package]] -category = "main" -description = "Python for Window Extensions" -marker = "platform_system == \"Windows\"" +name = "pyunormalize" +version = "16.0.0" +description = "Unicode normalization forms (NFC, NFKC, NFD, NFKD). A library independent of the Python core Unicode database." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "pyunormalize-16.0.0-py3-none-any.whl", hash = "sha256:c647d95e5d1e2ea9a2f448d1d95d8518348df24eab5c3fd32d2b5c3300a49152"}, + {file = "pyunormalize-16.0.0.tar.gz", hash = "sha256:2e1dfbb4a118154ae26f70710426a52a364b926c9191f764601f5a8cb12761f7"}, +] + +[[package]] name = "pywin32" +version = "306" +description = "Python for Window Extensions" optional = false python-versions = "*" -version = "228" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, + {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, + {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, + {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, + {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, + {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, + {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, + {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, + {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, + {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, + {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, + {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, + {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, + {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] [[package]] -category = "dev" -description = "Alternative regular expression module, to replace re." name = "regex" +version = "2022.10.31" +description = "Alternative regular expression module, to replace re." optional = false -python-versions = "*" -version = "2020.7.14" +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "regex-2022.10.31-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a8ff454ef0bb061e37df03557afda9d785c905dab15584860f982e88be73015f"}, + {file = "regex-2022.10.31-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1eba476b1b242620c266edf6325b443a2e22b633217a9835a52d8da2b5c051f9"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0e5af9a9effb88535a472e19169e09ce750c3d442fb222254a276d77808620b"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d03fe67b2325cb3f09be029fd5da8df9e6974f0cde2c2ac6a79d2634e791dd57"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9d0b68ac1743964755ae2d89772c7e6fb0118acd4d0b7464eaf3921c6b49dd4"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a45b6514861916c429e6059a55cf7db74670eaed2052a648e3e4d04f070e001"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8b0886885f7323beea6f552c28bff62cbe0983b9fbb94126531693ea6c5ebb90"}, + {file = "regex-2022.10.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5aefb84a301327ad115e9d346c8e2760009131d9d4b4c6b213648d02e2abe144"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:702d8fc6f25bbf412ee706bd73019da5e44a8400861dfff7ff31eb5b4a1276dc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a3c1ebd4ed8e76e886507c9eddb1a891673686c813adf889b864a17fafcf6d66"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:50921c140561d3db2ab9f5b11c5184846cde686bb5a9dc64cae442926e86f3af"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7db345956ecce0c99b97b042b4ca7326feeec6b75facd8390af73b18e2650ffc"}, + {file = "regex-2022.10.31-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:763b64853b0a8f4f9cfb41a76a4a85a9bcda7fdda5cb057016e7706fde928e66"}, + {file = "regex-2022.10.31-cp310-cp310-win32.whl", hash = "sha256:44136355e2f5e06bf6b23d337a75386371ba742ffa771440b85bed367c1318d1"}, + {file = "regex-2022.10.31-cp310-cp310-win_amd64.whl", hash = "sha256:bfff48c7bd23c6e2aec6454aaf6edc44444b229e94743b34bdcdda2e35126cf5"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b4b1fe58cd102d75ef0552cf17242705ce0759f9695334a56644ad2d83903fe"}, + {file = "regex-2022.10.31-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:542e3e306d1669b25936b64917285cdffcd4f5c6f0247636fec037187bd93542"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c27cc1e4b197092e50ddbf0118c788d9977f3f8f35bfbbd3e76c1846a3443df7"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8e38472739028e5f2c3a4aded0ab7eadc447f0d84f310c7a8bb697ec417229e"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76c598ca73ec73a2f568e2a72ba46c3b6c8690ad9a07092b18e48ceb936e9f0c"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c28d3309ebd6d6b2cf82969b5179bed5fefe6142c70f354ece94324fa11bf6a1"}, + {file = "regex-2022.10.31-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9af69f6746120998cd9c355e9c3c6aec7dff70d47247188feb4f829502be8ab4"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a5f9505efd574d1e5b4a76ac9dd92a12acb2b309551e9aa874c13c11caefbe4f"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5ff525698de226c0ca743bfa71fc6b378cda2ddcf0d22d7c37b1cc925c9650a5"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4fe7fda2fe7c8890d454f2cbc91d6c01baf206fbc96d89a80241a02985118c0c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:2cdc55ca07b4e70dda898d2ab7150ecf17c990076d3acd7a5f3b25cb23a69f1c"}, + {file = "regex-2022.10.31-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:44a6c2f6374e0033873e9ed577a54a3602b4f609867794c1a3ebba65e4c93ee7"}, + {file = "regex-2022.10.31-cp311-cp311-win32.whl", hash = "sha256:d8716f82502997b3d0895d1c64c3b834181b1eaca28f3f6336a71777e437c2af"}, + {file = "regex-2022.10.31-cp311-cp311-win_amd64.whl", hash = "sha256:61edbca89aa3f5ef7ecac8c23d975fe7261c12665f1d90a6b1af527bba86ce61"}, + {file = "regex-2022.10.31-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0a069c8483466806ab94ea9068c34b200b8bfc66b6762f45a831c4baaa9e8cdd"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d26166acf62f731f50bdd885b04b38828436d74e8e362bfcb8df221d868b5d9b"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac741bf78b9bb432e2d314439275235f41656e189856b11fb4e774d9f7246d81"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75f591b2055523fc02a4bbe598aa867df9e953255f0b7f7715d2a36a9c30065c"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bddd61d2a3261f025ad0f9ee2586988c6a00c780a2fb0a92cea2aa702c54"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef4163770525257876f10e8ece1cf25b71468316f61451ded1a6f44273eedeb5"}, + {file = "regex-2022.10.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7b280948d00bd3973c1998f92e22aa3ecb76682e3a4255f33e1020bd32adf443"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:d0213671691e341f6849bf33cd9fad21f7b1cb88b89e024f33370733fec58742"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:22e7ebc231d28393dfdc19b185d97e14a0f178bedd78e85aad660e93b646604e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:8ad241da7fac963d7573cc67a064c57c58766b62a9a20c452ca1f21050868dfa"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:586b36ebda81e6c1a9c5a5d0bfdc236399ba6595e1397842fd4a45648c30f35e"}, + {file = "regex-2022.10.31-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:0653d012b3bf45f194e5e6a41df9258811ac8fc395579fa82958a8b76286bea4"}, + {file = "regex-2022.10.31-cp36-cp36m-win32.whl", hash = "sha256:144486e029793a733e43b2e37df16a16df4ceb62102636ff3db6033994711066"}, + {file = "regex-2022.10.31-cp36-cp36m-win_amd64.whl", hash = "sha256:c14b63c9d7bab795d17392c7c1f9aaabbffd4cf4387725a0ac69109fb3b550c6"}, + {file = "regex-2022.10.31-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4cac3405d8dda8bc6ed499557625585544dd5cbf32072dcc72b5a176cb1271c8"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23cbb932cc53a86ebde0fb72e7e645f9a5eec1a5af7aa9ce333e46286caef783"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74bcab50a13960f2a610cdcd066e25f1fd59e23b69637c92ad470784a51b1347"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78d680ef3e4d405f36f0d6d1ea54e740366f061645930072d39bca16a10d8c93"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6910b56b700bea7be82c54ddf2e0ed792a577dfaa4a76b9af07d550af435c6"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:659175b2144d199560d99a8d13b2228b85e6019b6e09e556209dfb8c37b78a11"}, + {file = "regex-2022.10.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1ddf14031a3882f684b8642cb74eea3af93a2be68893901b2b387c5fd92a03ec"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b683e5fd7f74fb66e89a1ed16076dbab3f8e9f34c18b1979ded614fe10cdc4d9"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2bde29cc44fa81c0a0c8686992c3080b37c488df167a371500b2a43ce9f026d1"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4919899577ba37f505aaebdf6e7dc812d55e8f097331312db7f1aab18767cce8"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:9c94f7cc91ab16b36ba5ce476f1904c91d6c92441f01cd61a8e2729442d6fcf5"}, + {file = "regex-2022.10.31-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ae1e96785696b543394a4e3f15f3f225d44f3c55dafe3f206493031419fedf95"}, + {file = "regex-2022.10.31-cp37-cp37m-win32.whl", hash = "sha256:c670f4773f2f6f1957ff8a3962c7dd12e4be54d05839b216cb7fd70b5a1df394"}, + {file = "regex-2022.10.31-cp37-cp37m-win_amd64.whl", hash = "sha256:8e0caeff18b96ea90fc0eb6e3bdb2b10ab5b01a95128dfeccb64a7238decf5f0"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:131d4be09bea7ce2577f9623e415cab287a3c8e0624f778c1d955ec7c281bd4d"}, + {file = "regex-2022.10.31-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e613a98ead2005c4ce037c7b061f2409a1a4e45099edb0ef3200ee26ed2a69a8"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052b670fafbe30966bbe5d025e90b2a491f85dfe5b2583a163b5e60a85a321ad"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa62a07ac93b7cb6b7d0389d8ef57ffc321d78f60c037b19dfa78d6b17c928ee"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5352bea8a8f84b89d45ccc503f390a6be77917932b1c98c4cdc3565137acc714"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20f61c9944f0be2dc2b75689ba409938c14876c19d02f7585af4460b6a21403e"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29c04741b9ae13d1e94cf93fca257730b97ce6ea64cfe1eba11cf9ac4e85afb6"}, + {file = "regex-2022.10.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:543883e3496c8b6d58bd036c99486c3c8387c2fc01f7a342b760c1ea3158a318"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7a8b43ee64ca8f4befa2bea4083f7c52c92864d8518244bfa6e88c751fa8fff"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6a9a19bea8495bb419dc5d38c4519567781cd8d571c72efc6aa959473d10221a"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6ffd55b5aedc6f25fd8d9f905c9376ca44fcf768673ffb9d160dd6f409bfda73"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4bdd56ee719a8f751cf5a593476a441c4e56c9b64dc1f0f30902858c4ef8771d"}, + {file = "regex-2022.10.31-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ca88da1bd78990b536c4a7765f719803eb4f8f9971cc22d6ca965c10a7f2c4c"}, + {file = "regex-2022.10.31-cp38-cp38-win32.whl", hash = "sha256:5a260758454580f11dd8743fa98319bb046037dfab4f7828008909d0aa5292bc"}, + {file = "regex-2022.10.31-cp38-cp38-win_amd64.whl", hash = "sha256:5e6a5567078b3eaed93558842346c9d678e116ab0135e22eb72db8325e90b453"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5217c25229b6a85049416a5c1e6451e9060a1edcf988641e309dbe3ab26d3e49"}, + {file = "regex-2022.10.31-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4bf41b8b0a80708f7e0384519795e80dcb44d7199a35d52c15cc674d10b3081b"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf0da36a212978be2c2e2e2d04bdff46f850108fccc1851332bcae51c8907cc"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d403d781b0e06d2922435ce3b8d2376579f0c217ae491e273bab8d092727d244"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a37d51fa9a00d265cf73f3de3930fa9c41548177ba4f0faf76e61d512c774690"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4f781ffedd17b0b834c8731b75cce2639d5a8afe961c1e58ee7f1f20b3af185"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d243b36fbf3d73c25e48014961e83c19c9cc92530516ce3c43050ea6276a2ab7"}, + {file = "regex-2022.10.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:370f6e97d02bf2dd20d7468ce4f38e173a124e769762d00beadec3bc2f4b3bc4"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:597f899f4ed42a38df7b0e46714880fb4e19a25c2f66e5c908805466721760f5"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7dbdce0c534bbf52274b94768b3498abdf675a691fec5f751b6057b3030f34c1"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:22960019a842777a9fa5134c2364efaed5fbf9610ddc5c904bd3a400973b0eb8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7f5a3ffc731494f1a57bd91c47dc483a1e10048131ffb52d901bfe2beb6102e8"}, + {file = "regex-2022.10.31-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7ef6b5942e6bfc5706301a18a62300c60db9af7f6368042227ccb7eeb22d0892"}, + {file = "regex-2022.10.31-cp39-cp39-win32.whl", hash = "sha256:395161bbdbd04a8333b9ff9763a05e9ceb4fe210e3c7690f5e68cedd3d65d8e1"}, + {file = "regex-2022.10.31-cp39-cp39-win_amd64.whl", hash = "sha256:957403a978e10fb3ca42572a23e6f7badff39aa1ce2f4ade68ee452dc6807692"}, + {file = "regex-2022.10.31.tar.gz", hash = "sha256:a3a98921da9a1bf8457aeee6a551948a83601689e5ecdd736894ea9bbec77e83"}, +] [[package]] -category = "main" -description = "Python HTTP for Humans." name = "requests" +version = "2.34.2" +description = "Python HTTP for Humans." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "2.24.0" +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, + {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, +] [package.dependencies] -certifi = ">=2017.4.17" -chardet = ">=3.0.2,<4" -idna = ">=2.5,<3" -urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" +certifi = ">=2023.5.7" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.26,<3" [package.extras] -security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"] [[package]] -category = "main" -description = "A package for Recursive Length Prefix encoding and decoding" name = "rlp" +version = "5.0.0" +description = "rlp: A package for Recursive Length Prefix encoding and decoding" optional = false -python-versions = "*" -version = "1.2.0" +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "rlp-5.0.0-py3-none-any.whl", hash = "sha256:9525fb895d3a6540b7401eadd11c1b64740a5eccb82e543ac7e9735d319b0977"}, + {file = "rlp-5.0.0.tar.gz", hash = "sha256:ae8ac791160c160e270f9c7df76e68f4d42bb86a13726d807b9357c312d0bac4"}, +] [package.dependencies] -eth-utils = ">=1.0.2,<2" +eth-utils = ">=2" + +[package.extras] +rust-backend = ["rusty-rlp (>=0.2.1)"] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +description = "Manipulate well-formed Roman numerals" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7"}, + {file = "roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2"}, +] + +[[package]] +name = "ruff" +version = "0.16.0" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e"}, + {file = "ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522"}, + {file = "ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed"}, + {file = "ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb"}, + {file = "ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472"}, + {file = "ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d"}, + {file = "ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982"}, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6"}, + {file = "setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a"}, +] [package.extras] -dev = ["bumpversion (>=0.5.3,<1)", "setuptools (>=36.2.0)", "pytest-xdist", "pytest-watch (>=4.1.0,<5)", "wheel", "ipython", "twine", "pytest (3.3.2)", "tox (>=2.9.1,<3)", "hypothesis (3.56.5)", "flake8 (3.4.1)", "Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"] -doc = ["Sphinx (>=1.6.5,<2)", "sphinx-rtd-theme (>=0.1.9)"] -lint = ["flake8 (3.4.1)"] -test = ["pytest (3.3.2)", "tox (>=2.9.1,<3)", "hypothesis (3.56.5)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] [[package]] -category = "main" -description = "Python 2 and 3 compatibility utilities" -name = "six" +name = "roman-numerals" +version = "4.1.0" +description = "Manipulate well-formed Roman numerals" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -version = "1.15.0" +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7"}, + {file = "roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2"}, +] + +[[package]] +name = "ruff" +version = "0.16.0" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e"}, + {file = "ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522"}, + {file = "ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b"}, + {file = "ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09"}, + {file = "ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed"}, + {file = "ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb"}, + {file = "ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472"}, + {file = "ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d"}, + {file = "ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982"}, +] + +[[package]] +name = "setuptools" +version = "81.0.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6"}, + {file = "setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] [[package]] -category = "dev" -description = "Python Library for Tom's Obvious, Minimal Language" -name = "toml" +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" -version = "0.10.1" +groups = ["dev"] +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] [[package]] -category = "main" -description = "List processing tools and functional utilities" -name = "toolz" +name = "soupsieve" +version = "2.4" +description = "A modern CSS selector implementation for Beautiful Soup." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "0.10.0" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "soupsieve-2.4-py3-none-any.whl", hash = "sha256:49e5368c2cda80ee7e84da9dbe3e110b70a4575f196efb74e51b94549d921955"}, + {file = "soupsieve-2.4.tar.gz", hash = "sha256:e28dba9ca6c7c00173e34e4ba57448f0688bb681b7c5e8bf4971daafc093d69a"}, +] [[package]] -category = "dev" -description = "a fork of Python 2 and 3 ast modules with type comment support" -name = "typed-ast" +name = "sphinx" +version = "9.1.0" +description = "Python documentation generator" optional = false -python-versions = "*" -version = "1.4.1" +python-versions = ">=3.12" +groups = ["dev"] +files = [ + {file = "sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978"}, + {file = "sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.21,<0.23" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +roman-numerals = ">=1.0.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = ">=1.0.7" +sphinxcontrib-devhelp = ">=1.0.6" +sphinxcontrib-htmlhelp = ">=2.0.6" +sphinxcontrib-jsmath = ">=1.0.1" +sphinxcontrib-qthelp = ">=1.0.6" +sphinxcontrib-serializinghtml = ">=1.1.9" + +[[package]] +name = "sphinx-book-theme" +version = "1.4.0" +description = "A clean book theme for scientific explanations and documentation with Sphinx" +optional = false +python-versions = ">=3.11" +groups = ["dev"] +files = [ + {file = "sphinx_book_theme-1.4.0-py3-none-any.whl", hash = "sha256:10fd57d667ee0b565928c3ce95562e281c5d1b94bd5a2aafc5cd41a5d498dcd0"}, + {file = "sphinx_book_theme-1.4.0.tar.gz", hash = "sha256:fee3cd70573d9a8cd4ab380a3cb1f11139c24dcaa2cf47d06d7f924f1b44eaff"}, +] + +[package.dependencies] +pydata-sphinx-theme = "0.20.0" +sphinx = ">=8.2" + +[package.extras] +code-style = ["pre-commit"] +doc = ["ablog (>=0.11.13)", "folium", "ipywidgets", "matplotlib", "myst-nb", "nbclient", "numpy", "numpydoc", "pandas", "plotly", "sphinx-copybutton", "sphinx-design", "sphinx-examples", "sphinx-tabs", "sphinx-thebe", "sphinx-togglebutton", "sphinxcontrib-bibtex", "sphinxcontrib-youtube", "sphinxext-opengraph"] +test = ["beautifulsoup4", "coverage", "defusedxml", "myst-nb", "pytest", "pytest-cov", "pytest-regressions", "sphinx_thebe"] [[package]] -category = "main" -description = "Backported and Experimental Type Hints for Python 3.5+" -name = "typing-extensions" +name = "sphinx-click" +version = "4.4.0" +description = "Sphinx extension that automatically documents click applications" optional = false -python-versions = "*" -version = "3.7.4.2" +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "sphinx-click-4.4.0.tar.gz", hash = "sha256:cc67692bd28f482c7f01531c61b64e9d2f069bfcf3d24cbbb51d4a84a749fa48"}, + {file = "sphinx_click-4.4.0-py3-none-any.whl", hash = "sha256:2821c10a68fc9ee6ce7c92fad26540d8d8c8f45e6d7258f0e4fb7529ae8fab49"}, +] + +[package.dependencies] +click = ">=7.0" +docutils = "*" +sphinx = ">=2.0" [[package]] -category = "main" -description = "HTTP library with thread-safe connection pooling, file post, and more." -name = "urllib3" +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" -version = "1.25.9" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] [package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0.14)", "ipaddress"] -socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] [[package]] -category = "main" -description = "Simple python varint implementation" -name = "varint" +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false -python-versions = "*" -version = "1.0.2" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] [[package]] -category = "dev" -description = "Measures the displayed width of unicode strings in a terminal" -name = "wcwidth" +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false -python-versions = "*" -version = "0.2.5" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] [[package]] -category = "main" -description = "Web3.py" -name = "web3" +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false -python-versions = ">=3.6,<4" -version = "5.12.0" +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] -[package.dependencies] -eth-abi = ">=2.0.0b6,<3.0.0" -eth-account = ">=0.5.2,<0.6.0" -eth-typing = ">=2.0.0,<3.0.0" -eth-utils = ">=1.8.4,<2.0.0" -hexbytes = ">=0.1.0,<1.0.0" -ipfshttpclient = ">=0.4.13,<1" -jsonschema = ">=3.2.0,<4.0.0" -lru-dict = ">=1.1.6,<2.0.0" -protobuf = ">=3.10.0,<4" -pypiwin32 = ">=223" -requests = ">=2.16.0,<3.0.0" -websockets = ">=8.1.0,<9.0.0" - -[package.dependencies.eth-hash] -extras = ["pycryptodome"] -version = ">=0.2.0,<1.0.0" - -[package.dependencies.typing-extensions] -python = "<3.8" -version = ">=3.7.4.1,<4" +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] [package.extras] -dev = ["eth-tester (v0.5.0-beta.1)", "py-geth (>=2.2.0,<3)", "flake8 (3.4.1)", "isort (>=4.2.15,<4.3.5)", "mypy (0.730)", "mock", "sphinx-better-theme (>=0.1.4)", "click (>=5.1)", "configparser (3.5.0)", "contextlib2 (>=0.5.4)", "py-solc (>=0.4.0)", "pytest (>=4.4.0,<5.0.0)", "sphinx (>=2.4.4,<3)", "sphinx-rtd-theme (>=0.1.9)", "toposort (>=1.4)", "towncrier (>=19.2.0,<20)", "urllib3", "web3 (>=2.1.0)", "wheel", "bumpversion", "flaky (>=3.3.0)", "hypothesis (>=3.31.2)", "pytest-asyncio (>=0.10.0,<0.11)", "pytest-mock (>=1.10,<2)", "pytest-pythonpath (>=0.3)", "pytest-watch (>=4.2,<5)", "pytest-xdist (>=1.29,<2)", "setuptools (>=36.2.0)", "tox (>=1.8.0)", "tqdm (>4.32,<5)", "twine (>=1.13,<2)", "when-changed (>=0.3.0,<0.4)"] -docs = ["mock", "sphinx-better-theme (>=0.1.4)", "click (>=5.1)", "configparser (3.5.0)", "contextlib2 (>=0.5.4)", "py-geth (>=2.2.0,<3)", "py-solc (>=0.4.0)", "pytest (>=4.4.0,<5.0.0)", "sphinx (>=2.4.4,<3)", "sphinx-rtd-theme (>=0.1.9)", "toposort (>=1.4)", "towncrier (>=19.2.0,<20)", "urllib3", "web3 (>=2.1.0)", "wheel"] -linter = ["flake8 (3.4.1)", "isort (>=4.2.15,<4.3.5)", "mypy (0.730)"] -tester = ["eth-tester (v0.5.0-beta.1)", "py-geth (>=2.2.0,<3)"] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] [[package]] -category = "main" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -name = "websockets" +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false -python-versions = ">=3.6.1" -version = "8.1" +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] [[package]] -category = "main" -description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\"" -name = "zipp" +name = "toolz" +version = "0.12.0" +description = "List processing tools and functional utilities" optional = false -python-versions = ">=3.6" -version = "3.1.0" +python-versions = ">=3.5" +groups = ["main"] +files = [ + {file = "toolz-0.12.0-py3-none-any.whl", hash = "sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f"}, + {file = "toolz-0.12.0.tar.gz", hash = "sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194"}, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"}, + {file = "typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147"}, + {file = "typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47"}, +] + +[package.dependencies] +typing-extensions = ">=4.15.0" + +[[package]] +name = "urllib3" +version = "1.26.15" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +groups = ["main", "dev"] +files = [ + {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"}, + {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"}, +] [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] +brotli = ["brotli (>=1.0.9) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] -[metadata] -content-hash = "87fe8c1c54110ed79b43d3f9e68abb87f669a89b0c1eccfea774750670b34993" -python-versions = "^3.7" - -[metadata.files] -appdirs = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-19.3.0-py2.py3-none-any.whl", hash = "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c"}, - {file = "attrs-19.3.0.tar.gz", hash = "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"}, -] -base58 = [ - {file = "base58-2.0.1-py3-none-any.whl", hash = "sha256:447adc750d6b642987ffc6d397ecd15a799852d5f6a1d308d384500243825058"}, - {file = "base58-2.0.1.tar.gz", hash = "sha256:365c9561d9babac1b5f18ee797508cd54937a724b6e419a130abad69cec5ca79"}, -] -bitarray = [ - {file = "bitarray-1.2.2.tar.gz", hash = "sha256:27a69ffcee3b868abab3ce8b17c69e02b63e722d4d64ffd91d659f81e9984954"}, -] -black = [ - {file = "black-19.10b0-py36-none-any.whl", hash = "sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b"}, - {file = "black-19.10b0.tar.gz", hash = "sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"}, -] -certifi = [ - {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, - {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, -] -chardet = [ - {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, - {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, -] -click = [ - {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, - {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, -] -colorama = [ - {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, - {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, -] -coverage = [ - {file = "coverage-5.2-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:d9ad0a988ae20face62520785ec3595a5e64f35a21762a57d115dae0b8fb894a"}, - {file = "coverage-5.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:4bb385a747e6ae8a65290b3df60d6c8a692a5599dc66c9fa3520e667886f2e10"}, - {file = "coverage-5.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:9702e2cb1c6dec01fb8e1a64c015817c0800a6eca287552c47a5ee0ebddccf62"}, - {file = "coverage-5.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:42fa45a29f1059eda4d3c7b509589cc0343cd6bbf083d6118216830cd1a51613"}, - {file = "coverage-5.2-cp27-cp27m-win32.whl", hash = "sha256:41d88736c42f4a22c494c32cc48a05828236e37c991bd9760f8923415e3169e4"}, - {file = "coverage-5.2-cp27-cp27m-win_amd64.whl", hash = "sha256:bbb387811f7a18bdc61a2ea3d102be0c7e239b0db9c83be7bfa50f095db5b92a"}, - {file = "coverage-5.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3740b796015b889e46c260ff18b84683fa2e30f0f75a171fb10d2bf9fb91fc70"}, - {file = "coverage-5.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ebf2431b2d457ae5217f3a1179533c456f3272ded16f8ed0b32961a6d90e38ee"}, - {file = "coverage-5.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:d54d7ea74cc00482a2410d63bf10aa34ebe1c49ac50779652106c867f9986d6b"}, - {file = "coverage-5.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:87bdc8135b8ee739840eee19b184804e5d57f518578ffc797f5afa2c3c297913"}, - {file = "coverage-5.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ed9a21502e9223f563e071759f769c3d6a2e1ba5328c31e86830368e8d78bc9c"}, - {file = "coverage-5.2-cp35-cp35m-win32.whl", hash = "sha256:509294f3e76d3f26b35083973fbc952e01e1727656d979b11182f273f08aa80b"}, - {file = "coverage-5.2-cp35-cp35m-win_amd64.whl", hash = "sha256:ca63dae130a2e788f2b249200f01d7fa240f24da0596501d387a50e57aa7075e"}, - {file = "coverage-5.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:5c74c5b6045969b07c9fb36b665c9cac84d6c174a809fc1b21bdc06c7836d9a0"}, - {file = "coverage-5.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c32aa13cc3fe86b0f744dfe35a7f879ee33ac0a560684fef0f3e1580352b818f"}, - {file = "coverage-5.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:1e58fca3d9ec1a423f1b7f2aa34af4f733cbfa9020c8fe39ca451b6071237405"}, - {file = "coverage-5.2-cp36-cp36m-win32.whl", hash = "sha256:3b2c34690f613525672697910894b60d15800ac7e779fbd0fccf532486c1ba40"}, - {file = "coverage-5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:a4d511012beb967a39580ba7d2549edf1e6865a33e5fe51e4dce550522b3ac0e"}, - {file = "coverage-5.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:32ecee61a43be509b91a526819717d5e5650e009a8d5eda8631a59c721d5f3b6"}, - {file = "coverage-5.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6f91b4492c5cde83bfe462f5b2b997cdf96a138f7c58b1140f05de5751623cf1"}, - {file = "coverage-5.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bfcc811883699ed49afc58b1ed9f80428a18eb9166422bce3c31a53dba00fd1d"}, - {file = "coverage-5.2-cp37-cp37m-win32.whl", hash = "sha256:60a3d36297b65c7f78329b80120f72947140f45b5c7a017ea730f9112b40f2ec"}, - {file = "coverage-5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:12eaccd86d9a373aea59869bc9cfa0ab6ba8b1477752110cb4c10d165474f703"}, - {file = "coverage-5.2-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d82db1b9a92cb5c67661ca6616bdca6ff931deceebb98eecbd328812dab52032"}, - {file = "coverage-5.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:214eb2110217f2636a9329bc766507ab71a3a06a8ea30cdeebb47c24dce5972d"}, - {file = "coverage-5.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:8a3decd12e7934d0254939e2bf434bf04a5890c5bf91a982685021786a08087e"}, - {file = "coverage-5.2-cp38-cp38-win32.whl", hash = "sha256:1dcebae667b73fd4aa69237e6afb39abc2f27520f2358590c1b13dd90e32abe7"}, - {file = "coverage-5.2-cp38-cp38-win_amd64.whl", hash = "sha256:f50632ef2d749f541ca8e6c07c9928a37f87505ce3a9f20c8446ad310f1aa87b"}, - {file = "coverage-5.2-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:7403675df5e27745571aba1c957c7da2dacb537c21e14007ec3a417bf31f7f3d"}, - {file = "coverage-5.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0fc4e0d91350d6f43ef6a61f64a48e917637e1dcfcba4b4b7d543c628ef82c2d"}, - {file = "coverage-5.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:25fe74b5b2f1b4abb11e103bb7984daca8f8292683957d0738cd692f6a7cc64c"}, - {file = "coverage-5.2-cp39-cp39-win32.whl", hash = "sha256:d67599521dff98ec8c34cd9652cbcfe16ed076a2209625fca9dc7419b6370e5c"}, - {file = "coverage-5.2-cp39-cp39-win_amd64.whl", hash = "sha256:10f2a618a6e75adf64329f828a6a5b40244c1c50f5ef4ce4109e904e69c71bd2"}, - {file = "coverage-5.2.tar.gz", hash = "sha256:1874bdc943654ba46d28f179c1846f5710eda3aeb265ff029e0ac2b52daae404"}, -] -cytoolz = [ - {file = "cytoolz-0.10.1.tar.gz", hash = "sha256:82f5bba81d73a5a6b06f2a3553ff9003d865952fcb32e1df192378dd944d8a5c"}, -] -eth-abi = [ - {file = "eth_abi-2.1.1-py3-none-any.whl", hash = "sha256:78df5d2758247a8f0766a7cfcea4575bcfe568c34a33e6d05a72c328a9040444"}, - {file = "eth_abi-2.1.1.tar.gz", hash = "sha256:4bb1d87bb6605823379b07f6c02c8af45df01a27cc85bd6abb7cf1446ce7d188"}, -] -eth-account = [ - {file = "eth-account-0.5.2.tar.gz", hash = "sha256:4289aa23a4beaf2e4c8caf4a68ceb10296897ca37f87a60f1d1503e0e6906a5b"}, - {file = "eth_account-0.5.2-py3-none-any.whl", hash = "sha256:93996bee1500acb8f0c858573374b8860cbaec98bcc23228a88ea8b63bb2979a"}, -] -eth-hash = [ - {file = "eth-hash-0.2.0.tar.gz", hash = "sha256:499dc02d098f69856d1a6dd005529c16174157d4fb2a9fe20c41f69e39f8f176"}, - {file = "eth_hash-0.2.0-py3-none-any.whl", hash = "sha256:1b9cb34dd3cd99c85c2bd6a1420ceae39a2eee8bf080efd264bcda8be3edecc8"}, -] -eth-keyfile = [ - {file = "eth-keyfile-0.5.1.tar.gz", hash = "sha256:939540efb503380bc30d926833e6a12b22c6750de80feef3720d79e5a79de47d"}, - {file = "eth_keyfile-0.5.1-py3-none-any.whl", hash = "sha256:70d734af17efdf929a90bb95375f43522be4ed80c3b9e0a8bca575fb11cd1159"}, -] -eth-keys = [ - {file = "eth-keys-0.3.3.tar.gz", hash = "sha256:a9a1e83e443bd369265b1a1b66dc30f6841bdbb3577ecd042e037b7b405b6cb0"}, - {file = "eth_keys-0.3.3-py3-none-any.whl", hash = "sha256:412dd5c9732b8e92af40c9c77597f4661c57eba3897aaa55e527af56a8c5ab47"}, -] -eth-rlp = [ - {file = "eth-rlp-0.1.2.tar.gz", hash = "sha256:05d8456981d85e16a9afa57f2f2c3356af5d1c49499cc8512cfcdc034b90dde5"}, - {file = "eth_rlp-0.1.2-py3-none-any.whl", hash = "sha256:a94744c207ea731a7266bd0894179dc6e51a6a8965316000c8e823b5d7e07694"}, -] -eth-typing = [ - {file = "eth-typing-2.2.1.tar.gz", hash = "sha256:cf9e5e9fb62cfeb1027823328569315166851c65c5774604d801b6b926ff65bc"}, - {file = "eth_typing-2.2.1-py3-none-any.whl", hash = "sha256:2f3e1f891226148898b219bd94674a9af06c2d75d8cdd8c6722227b472cbd4d4"}, -] -eth-utils = [ - {file = "eth-utils-1.9.0.tar.gz", hash = "sha256:677d68dfcdc60875459d6e1a0a9ffb80117f9a204096158931f94329ba477202"}, - {file = "eth_utils-1.9.0-py3-none-any.whl", hash = "sha256:3f03da7c445c73e9c353dbb400853caf75da988a5884d8b4fafa6c5d21641352"}, -] -flake8 = [ - {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, - {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, -] -hexbytes = [ - {file = "hexbytes-0.2.1-py3-none-any.whl", hash = "sha256:a093a5533aa63ca6614246fa97feb693b5813f9e736c38b68fe4e2d8fcc35aa5"}, - {file = "hexbytes-0.2.1.tar.gz", hash = "sha256:123fcf397f52fc7eb34f43ca9a7930a6acfebcabe8ffaef6c7d3520c2356345a"}, -] -idna = [ - {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, - {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, -] -importlib-metadata = [ - {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, - {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, -] -ipfshttpclient = [ - {file = "ipfshttpclient-0.6.0.post1-py3-none-any.whl", hash = "sha256:3f7b10919ca4158957aad194c20e34e8fb72296d9af015bd248a736ac9de1e6f"}, - {file = "ipfshttpclient-0.6.0.post1.tar.gz", hash = "sha256:d777d91562b23813cf7aa17dbe362958087c3fcc8b98a16cbe42c562a7724055"}, -] -jsonschema = [ - {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, - {file = "jsonschema-3.2.0.tar.gz", hash = "sha256:c8a85b28d377cc7737e46e2d9f2b4f44ee3c0e1deac6bf46ddefc7187d30797a"}, -] -lru-dict = [ - {file = "lru-dict-1.1.6.tar.gz", hash = "sha256:365457660e3d05b76f1aba3e0f7fedbfcd6528e97c5115a351ddd0db488354cc"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -more-itertools = [ - {file = "more-itertools-8.4.0.tar.gz", hash = "sha256:68c70cc7167bdf5c7c9d8f6954a7837089c6a36bf565383919bb595efb8a17e5"}, - {file = "more_itertools-8.4.0-py3-none-any.whl", hash = "sha256:b78134b2063dd214000685165d81c154522c3ee0a1c0d4d113c80361c234c5a2"}, -] -multiaddr = [ - {file = "multiaddr-0.0.9-py2.py3-none-any.whl", hash = "sha256:5c0f862cbcf19aada2a899f80ef896ddb2e85614e0c8f04dd287c06c69dac95b"}, - {file = "multiaddr-0.0.9.tar.gz", hash = "sha256:30b2695189edc3d5b90f1c303abb8f02d963a3a4edf2e7178b975eb417ab0ecf"}, -] -mypy = [ - {file = "mypy-0.782-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c"}, - {file = "mypy-0.782-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e"}, - {file = "mypy-0.782-cp35-cp35m-win_amd64.whl", hash = "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d"}, - {file = "mypy-0.782-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd"}, - {file = "mypy-0.782-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"}, - {file = "mypy-0.782-cp36-cp36m-win_amd64.whl", hash = "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406"}, - {file = "mypy-0.782-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86"}, - {file = "mypy-0.782-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707"}, - {file = "mypy-0.782-cp37-cp37m-win_amd64.whl", hash = "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308"}, - {file = "mypy-0.782-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc"}, - {file = "mypy-0.782-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea"}, - {file = "mypy-0.782-cp38-cp38-win_amd64.whl", hash = "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b"}, - {file = "mypy-0.782-py3-none-any.whl", hash = "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d"}, - {file = "mypy-0.782.tar.gz", hash = "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -netaddr = [ - {file = "netaddr-0.8.0-py2.py3-none-any.whl", hash = "sha256:9666d0232c32d2656e5e5f8d735f58fd6c7457ce52fc21c98d45f2af78f990ac"}, - {file = "netaddr-0.8.0.tar.gz", hash = "sha256:d6cc57c7a07b1d9d2e917aa8b36ae8ce61c35ba3fcd1b83ca31c5a0ee2b5a243"}, -] -packaging = [ - {file = "packaging-20.4-py2.py3-none-any.whl", hash = "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"}, - {file = "packaging-20.4.tar.gz", hash = "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8"}, -] -parsimonious = [ - {file = "parsimonious-0.8.1.tar.gz", hash = "sha256:3add338892d580e0cb3b1a39e4a1b427ff9f687858fdd61097053742391a9f6b"}, -] -pathspec = [ - {file = "pathspec-0.8.0-py2.py3-none-any.whl", hash = "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0"}, - {file = "pathspec-0.8.0.tar.gz", hash = "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"}, -] -pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, -] -protobuf = [ - {file = "protobuf-3.12.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e1464a4a2cf12f58f662c8e6421772c07947266293fb701cb39cd9c1e183f63c"}, - {file = "protobuf-3.12.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:6f349adabf1c004aba53f7b4633459f8ca8a09654bf7e69b509c95a454755776"}, - {file = "protobuf-3.12.2-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:be04fe14ceed7f8641e30f36077c1a654ff6f17d0c7a5283b699d057d150d82a"}, - {file = "protobuf-3.12.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f4b73736108a416c76c17a8a09bc73af3d91edaa26c682aaa460ef91a47168d3"}, - {file = "protobuf-3.12.2-cp35-cp35m-win32.whl", hash = "sha256:5524c7020eb1fb7319472cb75c4c3206ef18b34d6034d2ee420a60f99cddeb07"}, - {file = "protobuf-3.12.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bff02030bab8b969f4de597543e55bd05e968567acb25c0a87495a31eb09e925"}, - {file = "protobuf-3.12.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c9ca9f76805e5a637605f171f6c4772fc4a81eced4e2f708f79c75166a2c99ea"}, - {file = "protobuf-3.12.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:304e08440c4a41a0f3592d2a38934aad6919d692bb0edfb355548786728f9a5e"}, - {file = "protobuf-3.12.2-cp36-cp36m-win32.whl", hash = "sha256:b5a114ea9b7fc90c2cc4867a866512672a47f66b154c6d7ee7e48ddb68b68122"}, - {file = "protobuf-3.12.2-cp36-cp36m-win_amd64.whl", hash = "sha256:85b94d2653b0fdf6d879e39d51018bf5ccd86c81c04e18a98e9888694b98226f"}, - {file = "protobuf-3.12.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a7ab28a8f1f043c58d157bceb64f80e4d2f7f1b934bc7ff5e7f7a55a337ea8b0"}, - {file = "protobuf-3.12.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:eafe9fa19fcefef424ee089fb01ac7177ff3691af7cc2ae8791ae523eb6ca907"}, - {file = "protobuf-3.12.2-cp37-cp37m-win32.whl", hash = "sha256:612bc97e42b22af10ba25e4140963fbaa4c5181487d163f4eb55b0b15b3dfcd2"}, - {file = "protobuf-3.12.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e72736dd822748b0721f41f9aaaf6a5b6d5cfc78f6c8690263aef8bba4457f0e"}, - {file = "protobuf-3.12.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:87535dc2d2ef007b9d44e309d2b8ea27a03d2fa09556a72364d706fcb7090828"}, - {file = "protobuf-3.12.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:50b5fee674878b14baea73b4568dc478c46a31dd50157a5b5d2f71138243b1a9"}, - {file = "protobuf-3.12.2-py2.py3-none-any.whl", hash = "sha256:a96f8fc625e9ff568838e556f6f6ae8eca8b4837cdfb3f90efcb7c00e342a2eb"}, - {file = "protobuf-3.12.2.tar.gz", hash = "sha256:49ef8ab4c27812a89a76fa894fe7a08f42f2147078392c0dee51d4a444ef6df5"}, -] -py = [ - {file = "py-1.9.0-py2.py3-none-any.whl", hash = "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2"}, - {file = "py-1.9.0.tar.gz", hash = "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"}, -] -pycodestyle = [ - {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, - {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, -] -pycryptodome = [ - {file = "pycryptodome-3.9.8-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:50348edd283afdccddc0938cdc674484533912ba8a99a27c7bfebb75030aa856"}, - {file = "pycryptodome-3.9.8-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:80d57177a0b7c14d4594c62bbb47fe2f6309ad3b0a34348a291d570925c97a82"}, - {file = "pycryptodome-3.9.8-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:fbe65d5cfe04ff2f7684160d50f5118bdefb01e3af4718eeb618bfed40f19d94"}, - {file = "pycryptodome-3.9.8-cp27-cp27m-win32.whl", hash = "sha256:bcd5b8416e73e4b0d48afba3704d8c826414764dafaed7a1a93c442188d90ccc"}, - {file = "pycryptodome-3.9.8-cp27-cp27m-win_amd64.whl", hash = "sha256:360955eece2cd0fa694a708d10303c6abd7b39614fa2547b6bd245da76198beb"}, - {file = "pycryptodome-3.9.8-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:1e655746f539421d923fd48df8f6f40b3443d80b75532501c0085b64afed9df5"}, - {file = "pycryptodome-3.9.8-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:709b9f144d23e290b9863121d1ace14a72e01f66ea9c903fbdc690520dfdfcf0"}, - {file = "pycryptodome-3.9.8-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:6276478ada411aca97c0d5104916354b3d740d368407912722bd4d11aa9ee4c2"}, - {file = "pycryptodome-3.9.8-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:54bdedd28476dea8a3cd86cb67c0df1f0e3d71cae8022354b0f879c41a3d27b2"}, - {file = "pycryptodome-3.9.8-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f521178e5a991ffd04182ed08f552daca1affcb826aeda0e1945cd989a9d4345"}, - {file = "pycryptodome-3.9.8-cp35-cp35m-win32.whl", hash = "sha256:a207231a52426de3ff20f5608f0687261a3329d97a036c51f7d4c606a6f30c23"}, - {file = "pycryptodome-3.9.8-cp35-cp35m-win_amd64.whl", hash = "sha256:2b998dc45ef5f4e5cf5248a6edfcd8d8e9fb5e35df8e4259b13a1b10eda7b16b"}, - {file = "pycryptodome-3.9.8-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:03d5cca8618620f45fd40f827423f82b86b3a202c8d44108601b0f5f56b04299"}, - {file = "pycryptodome-3.9.8-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:f78a68c2c820e4731e510a2df3eef0322f24fde1781ced970bf497b6c7d92982"}, - {file = "pycryptodome-3.9.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:132a56abba24e2e06a479d8e5db7a48271a73a215f605017bbd476d31f8e71c1"}, - {file = "pycryptodome-3.9.8-cp36-cp36m-win32.whl", hash = "sha256:67dcad1b8b201308586a8ca2ffe89df1e4f731d5a4cdd0610cc4ea790351c739"}, - {file = "pycryptodome-3.9.8-cp36-cp36m-win_amd64.whl", hash = "sha256:b56638d58a3a4be13229c6a815cd448f9e3ce40c00880a5398471b42ee86f50e"}, - {file = "pycryptodome-3.9.8-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:bec2bcdf7c9ce7f04d718e51887f3b05dc5c1cfaf5d2c2e9065ecddd1b2f6c9a"}, - {file = "pycryptodome-3.9.8-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:abc2e126c9490e58a36a0f83516479e781d83adfb134576a5cbe5c6af2a3e93c"}, - {file = "pycryptodome-3.9.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ef39c98d9b8c0736d91937d193653e47c3b19ddf4fc3bccdc5e09aaa4b0c5d21"}, - {file = "pycryptodome-3.9.8-cp37-cp37m-win32.whl", hash = "sha256:4350a42028240c344ee855f032c7d4ad6ff4f813bfbe7121547b7dc579ecc876"}, - {file = "pycryptodome-3.9.8-cp37-cp37m-win_amd64.whl", hash = "sha256:c8bf40cf6e281a4378e25846924327e728a887e8bf0ee83b2604a0f4b61692e8"}, - {file = "pycryptodome-3.9.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d8074c8448cfd0705dfa71ca333277fce9786d0b9cac75d120545de6253f996a"}, - {file = "pycryptodome-3.9.8-cp38-cp38-manylinux1_i686.whl", hash = "sha256:8063a712fba642f78d3c506b0896846601b6de7f5c3d534e388ad0cc07f5a149"}, - {file = "pycryptodome-3.9.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:dd302b6ae3965afeb5ef1b0d92486f986c0e65183cd7835973f0b593800590e6"}, - {file = "pycryptodome-3.9.8-cp38-cp38-win32.whl", hash = "sha256:02e51e1d5828d58f154896ddfd003e2e7584869c275e5acbe290443575370fba"}, - {file = "pycryptodome-3.9.8-cp38-cp38-win_amd64.whl", hash = "sha256:55eb61aca2c883db770999f50d091ff7c14016f2769ad7bca3d9b75d1d7c1b68"}, - {file = "pycryptodome-3.9.8-cp39-cp39-manylinux1_i686.whl", hash = "sha256:39ef9fb52d6ec7728fce1f1693cb99d60ce302aeebd59bcedea70ca3203fda60"}, - {file = "pycryptodome-3.9.8-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:de6e1cd75677423ff64712c337521e62e3a7a4fc84caabbd93207752e831a85a"}, - {file = "pycryptodome-3.9.8.tar.gz", hash = "sha256:0e24171cf01021bc5dc17d6a9d4f33a048f09d62cc3f62541e95ef104588bda4"}, -] -pyflakes = [ - {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, - {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, -] -pyparsing = [ - {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, - {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, -] -pypiwin32 = [ - {file = "pypiwin32-223-py3-none-any.whl", hash = "sha256:67adf399debc1d5d14dffc1ab5acacb800da569754fafdc576b2a039485aa775"}, - {file = "pypiwin32-223.tar.gz", hash = "sha256:71be40c1fbd28594214ecaecb58e7aa8b708eabfa0125c8a109ebd51edbd776a"}, -] -pyrsistent = [ - {file = "pyrsistent-0.16.0.tar.gz", hash = "sha256:28669905fe725965daa16184933676547c5bb40a5153055a8dee2a4bd7933ad3"}, -] -pytest = [ - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, -] -pytest-cov = [ - {file = "pytest-cov-2.10.0.tar.gz", hash = "sha256:1a629dc9f48e53512fcbfda6b07de490c374b0c83c55ff7a1720b3fccff0ac87"}, - {file = "pytest_cov-2.10.0-py2.py3-none-any.whl", hash = "sha256:6e6d18092dce6fad667cd7020deed816f858ad3b49d5b5e2b1cc1c97a4dba65c"}, -] -pytest-dotenv = [ - {file = "pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732"}, - {file = "pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f"}, +[[package]] +name = "virtualenv" +version = "20.23.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "virtualenv-20.23.0-py3-none-any.whl", hash = "sha256:6abec7670e5802a528357fdc75b26b9f57d5d92f29c5462ba0fbe45feacc685e"}, + {file = "virtualenv-20.23.0.tar.gz", hash = "sha256:a85caa554ced0c0afbd0d638e7e2d7b5f92d23478d05d17a76daeac8f279f924"}, +] + +[package.dependencies] +distlib = ">=0.3.6,<1" +filelock = ">=3.11,<4" +platformdirs = ">=3.2,<4" + +[package.extras] +docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2) ; platform_python_implementation == \"PyPy\"", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.7.1)", "time-machine (>=2.9) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "web3" +version = "8.0.0" +description = "web3: A Python library for interacting with Ethereum" +optional = false +python-versions = "<4,>=3.10" +groups = ["main"] +files = [ + {file = "web3-8.0.0-py3-none-any.whl", hash = "sha256:2276762aa5d86807eabad5af5c585a84bd6d9a706a613f8456c645a9669ffeb5"}, + {file = "web3-8.0.0.tar.gz", hash = "sha256:8d898fe1909037d6269c998d27c1806ce201cf6d7e49514ad6f3e4dcc64e6e9a"}, ] -python-dotenv = [ - {file = "python-dotenv-0.14.0.tar.gz", hash = "sha256:8c10c99a1b25d9a68058a1ad6f90381a62ba68230ca93966882a4dbc3bc9c33d"}, - {file = "python_dotenv-0.14.0-py2.py3-none-any.whl", hash = "sha256:c10863aee750ad720f4f43436565e4c1698798d763b63234fb5021b6c616e423"}, -] -pywin32 = [ - {file = "pywin32-228-cp27-cp27m-win32.whl", hash = "sha256:37dc9935f6a383cc744315ae0c2882ba1768d9b06700a70f35dc1ce73cd4ba9c"}, - {file = "pywin32-228-cp27-cp27m-win_amd64.whl", hash = "sha256:11cb6610efc2f078c9e6d8f5d0f957620c333f4b23466931a247fb945ed35e89"}, - {file = "pywin32-228-cp35-cp35m-win32.whl", hash = "sha256:1f45db18af5d36195447b2cffacd182fe2d296849ba0aecdab24d3852fbf3f80"}, - {file = "pywin32-228-cp35-cp35m-win_amd64.whl", hash = "sha256:6e38c44097a834a4707c1b63efa9c2435f5a42afabff634a17f563bc478dfcc8"}, - {file = "pywin32-228-cp36-cp36m-win32.whl", hash = "sha256:ec16d44b49b5f34e99eb97cf270806fdc560dff6f84d281eb2fcb89a014a56a9"}, - {file = "pywin32-228-cp36-cp36m-win_amd64.whl", hash = "sha256:a60d795c6590a5b6baeacd16c583d91cce8038f959bd80c53bd9a68f40130f2d"}, - {file = "pywin32-228-cp37-cp37m-win32.whl", hash = "sha256:af40887b6fc200eafe4d7742c48417529a8702dcc1a60bf89eee152d1d11209f"}, - {file = "pywin32-228-cp37-cp37m-win_amd64.whl", hash = "sha256:00eaf43dbd05ba6a9b0080c77e161e0b7a601f9a3f660727a952e40140537de7"}, - {file = "pywin32-228-cp38-cp38-win32.whl", hash = "sha256:fa6ba028909cfc64ce9e24bcf22f588b14871980d9787f1e2002c99af8f1850c"}, - {file = "pywin32-228-cp38-cp38-win_amd64.whl", hash = "sha256:9b3466083f8271e1a5eb0329f4e0d61925d46b40b195a33413e0905dccb285e8"}, - {file = "pywin32-228-cp39-cp39-win32.whl", hash = "sha256:ed74b72d8059a6606f64842e7917aeee99159ebd6b8d6261c518d002837be298"}, - {file = "pywin32-228-cp39-cp39-win_amd64.whl", hash = "sha256:8319bafdcd90b7202c50d6014efdfe4fde9311b3ff15fd6f893a45c0868de203"}, -] -regex = [ - {file = "regex-2020.7.14-cp27-cp27m-win32.whl", hash = "sha256:e46d13f38cfcbb79bfdb2964b0fe12561fe633caf964a77a5f8d4e45fe5d2ef7"}, - {file = "regex-2020.7.14-cp27-cp27m-win_amd64.whl", hash = "sha256:6961548bba529cac7c07af2fd4d527c5b91bb8fe18995fed6044ac22b3d14644"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c50a724d136ec10d920661f1442e4a8b010a4fe5aebd65e0c2241ea41dbe93dc"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8a51f2c6d1f884e98846a0a9021ff6861bdb98457879f412fdc2b42d14494067"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:9c568495e35599625f7b999774e29e8d6b01a6fb684d77dee1f56d41b11b40cd"}, - {file = "regex-2020.7.14-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:51178c738d559a2d1071ce0b0f56e57eb315bcf8f7d4cf127674b533e3101f88"}, - {file = "regex-2020.7.14-cp36-cp36m-win32.whl", hash = "sha256:9eddaafb3c48e0900690c1727fba226c4804b8e6127ea409689c3bb492d06de4"}, - {file = "regex-2020.7.14-cp36-cp36m-win_amd64.whl", hash = "sha256:14a53646369157baa0499513f96091eb70382eb50b2c82393d17d7ec81b7b85f"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1269fef3167bb52631ad4fa7dd27bf635d5a0790b8e6222065d42e91bede4162"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d0a5095d52b90ff38592bbdc2644f17c6d495762edf47d876049cfd2968fbccf"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:4c037fd14c5f4e308b8370b447b469ca10e69427966527edcab07f52d88388f7"}, - {file = "regex-2020.7.14-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:bc3d98f621898b4a9bc7fecc00513eec8f40b5b83913d74ccb445f037d58cd89"}, - {file = "regex-2020.7.14-cp37-cp37m-win32.whl", hash = "sha256:46bac5ca10fb748d6c55843a931855e2727a7a22584f302dd9bb1506e69f83f6"}, - {file = "regex-2020.7.14-cp37-cp37m-win_amd64.whl", hash = "sha256:0dc64ee3f33cd7899f79a8d788abfbec168410be356ed9bd30bbd3f0a23a7204"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5ea81ea3dbd6767873c611687141ec7b06ed8bab43f68fad5b7be184a920dc99"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bbb332d45b32df41200380fff14712cb6093b61bd142272a10b16778c418e98e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:c11d6033115dc4887c456565303f540c44197f4fc1a2bfb192224a301534888e"}, - {file = "regex-2020.7.14-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:75aaa27aa521a182824d89e5ab0a1d16ca207318a6b65042b046053cfc8ed07a"}, - {file = "regex-2020.7.14-cp38-cp38-win32.whl", hash = "sha256:d6cff2276e502b86a25fd10c2a96973fdb45c7a977dca2138d661417f3728341"}, - {file = "regex-2020.7.14-cp38-cp38-win_amd64.whl", hash = "sha256:7a2dd66d2d4df34fa82c9dc85657c5e019b87932019947faece7983f2089a840"}, - {file = "regex-2020.7.14.tar.gz", hash = "sha256:3a3af27a8d23143c49a3420efe5b3f8cf1a48c6fc8bc6856b03f638abc1833bb"}, -] -requests = [ - {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, - {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, -] -rlp = [ - {file = "rlp-1.2.0-py2.py3-none-any.whl", hash = "sha256:97b7e770f16442772311b33e6bc28b45318e7c8def69b9df16452304e224e9df"}, - {file = "rlp-1.2.0.tar.gz", hash = "sha256:27273fc2dbc3513c1e05ea6b8af28aac8745fb09c164e39e2ed2807bf7e1b342"}, -] -six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, -] -toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, -] -toolz = [ - {file = "toolz-0.10.0.tar.gz", hash = "sha256:08fdd5ef7c96480ad11c12d472de21acd32359996f69a5259299b540feba4560"}, -] -typed-ast = [ - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3"}, - {file = "typed_ast-1.4.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win32.whl", hash = "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919"}, - {file = "typed_ast-1.4.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01"}, - {file = "typed_ast-1.4.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652"}, - {file = "typed_ast-1.4.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win32.whl", hash = "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1"}, - {file = "typed_ast-1.4.1-cp36-cp36m-win_amd64.whl", hash = "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa"}, - {file = "typed_ast-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41"}, - {file = "typed_ast-1.4.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win32.whl", hash = "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe"}, - {file = "typed_ast-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355"}, - {file = "typed_ast-1.4.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907"}, - {file = "typed_ast-1.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d"}, - {file = "typed_ast-1.4.1-cp38-cp38-win32.whl", hash = "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c"}, - {file = "typed_ast-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4"}, - {file = "typed_ast-1.4.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34"}, - {file = "typed_ast-1.4.1.tar.gz", hash = "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b"}, -] -typing-extensions = [ - {file = "typing_extensions-3.7.4.2-py2-none-any.whl", hash = "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"}, - {file = "typing_extensions-3.7.4.2-py3-none-any.whl", hash = "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5"}, - {file = "typing_extensions-3.7.4.2.tar.gz", hash = "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae"}, -] -urllib3 = [ - {file = "urllib3-1.25.9-py2.py3-none-any.whl", hash = "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115"}, - {file = "urllib3-1.25.9.tar.gz", hash = "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527"}, -] -varint = [ - {file = "varint-1.0.2.tar.gz", hash = "sha256:a6ecc02377ac5ee9d65a6a8ad45c9ff1dac8ccee19400a5950fb51d594214ca5"}, -] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] -web3 = [ - {file = "web3-5.12.0-py3-none-any.whl", hash = "sha256:b17c17809364aa54bd06d2834f96aded9da416426b860bfb0c245c65b27810d5"}, - {file = "web3-5.12.0.tar.gz", hash = "sha256:f378a773886122d2126d7ba81e9f5b6d2e7b4def7e88543699bf85a951835942"}, -] -websockets = [ - {file = "websockets-8.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c"}, - {file = "websockets-8.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170"}, - {file = "websockets-8.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8"}, - {file = "websockets-8.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb"}, - {file = "websockets-8.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5"}, - {file = "websockets-8.1-cp36-cp36m-win32.whl", hash = "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a"}, - {file = "websockets-8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5"}, - {file = "websockets-8.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989"}, - {file = "websockets-8.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d"}, - {file = "websockets-8.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779"}, - {file = "websockets-8.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8"}, - {file = "websockets-8.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422"}, - {file = "websockets-8.1-cp37-cp37m-win32.whl", hash = "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc"}, - {file = "websockets-8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308"}, - {file = "websockets-8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092"}, - {file = "websockets-8.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485"}, - {file = "websockets-8.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1"}, - {file = "websockets-8.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55"}, - {file = "websockets-8.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824"}, - {file = "websockets-8.1-cp38-cp38-win32.whl", hash = "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36"}, - {file = "websockets-8.1-cp38-cp38-win_amd64.whl", hash = "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b"}, - {file = "websockets-8.1.tar.gz", hash = "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f"}, -] -zipp = [ - {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, + +[package.dependencies] +aiohttp = ">=3.7.4.post0" +eth-abi = ">=6" +eth-account = ">=0.14" +eth-hash = {version = ">=0.5.1", extras = ["pycryptodome"]} +eth-typing = ">=5.0.0" +eth-utils = ">=5.3.0" +hexbytes = ">=2" +pydantic = ">=2.4" +pyunormalize = ">=15" +pywin32 = {version = ">=223", markers = "platform_system == \"Windows\""} +requests = ">=2.23.0" +websockets = ">=14" + +[package.extras] +tester = ["eth-tester[py-evm] (>=0.14.0b1,<0.15.0b1)"] + +[[package]] +name = "websockets" +version = "17.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "websockets-17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:88b882764ef65147a7a5ae13168dedbe225a04e2ff4858fe543f2c402f093e9c"}, + {file = "websockets-17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98a5b2589a56a4b4f098b0a958099a4356ab904a7844f1da3841efca469af7e9"}, + {file = "websockets-17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:020e271205f8ab3406d7a59cd00de6dec722315924411c421bd00642f18bad86"}, + {file = "websockets-17.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:65be6bda2b537fefa4b3a5ccd6ab386533ce39dd8fe62433ec90901fdc81752d"}, + {file = "websockets-17.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c84bdef916556cbe1d5a43b423398be4dd3cba6522b463e53d848578b920695"}, + {file = "websockets-17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47a62d6045c6eaa0d8f97bc2fb68b8cf90077a0cbfd4e83d6f2d2145611ee134"}, + {file = "websockets-17.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34879e19bb0a3c44f9317679435aea5327fac993933a704cbf353bf1234b10c7"}, + {file = "websockets-17.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2d72879819f5145a342d0030c418702496c65a4b913ef81f5ae944dd91dd50f6"}, + {file = "websockets-17.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f25e099fdfe3b09f953d84698f729a1f7d1e99101b2787d7a28ed77b323750"}, + {file = "websockets-17.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:469355ab1af100b9380f1afb09985019f4a4b94fa1dd0e9396db4361626d7ab8"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00679b7468b4c2b12b0757118174e8eabac56bb2f579a928a104d9554a56e098"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a9fe648abd1d9b89aebfa30407bfdd08a0271ec5dc7d44a4c6ccd1ce22cf562a"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f47aafd92aa28b941180e6da8a42b0f711851b14b81a5b6bb28dbbb1fa35152c"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c89406fa3dcd4aa8662c6406cc5c0de1790e9614d2c3aaf03ca53a8a8ccf3405"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:b3b451fd2723ad3191a209afe6f3f4bc86c83e9a85bdc255353b91803ee6aa66"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:054c28db2dcec0e857e3b705d8c28012613e555b38c765d6a4f75340a4fc06a0"}, + {file = "websockets-17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f8e822efd54137d8cc8310eb64635ab827a4a6c72ff08691f38aa624776d8ecb"}, + {file = "websockets-17.1-cp311-cp311-win32.whl", hash = "sha256:dcb8d5f7edef7a399d322cf28d4c4e6f98dab64d301c8f50581a1080e5198142"}, + {file = "websockets-17.1-cp311-cp311-win_amd64.whl", hash = "sha256:b1bc819c6db90e8f91a38250a1ab4c058261871aa52d2fe36382eddedf146dee"}, + {file = "websockets-17.1-cp311-cp311-win_arm64.whl", hash = "sha256:edadce7a22052056fd4384543019856b34850363c9d387929f677ae01d79709c"}, + {file = "websockets-17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:76dd004f59115087c7b700474cb18f01325e37250032e19396c08ae41448e4b3"}, + {file = "websockets-17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:581fa678ef46f4277cc8491312468e582f8ad609dbab907ba6096a08c6a0ff98"}, + {file = "websockets-17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87f0d5e77548b0c40c8464cdb6108792e7e53f487c6400028a4ec28a8afbe5ab"}, + {file = "websockets-17.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:882af300d2c6a092b93767d5de03c7bb56dfb06314140c8e872d3f48e09f7b74"}, + {file = "websockets-17.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c863507ada5805517ca6dff1c524dcd42942efe6304dacf06700878398d21a6"}, + {file = "websockets-17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d41ef69d5416fbc1d98cf96c37be6192d10fd101c3e0f8b3ddc36e09432b3c08"}, + {file = "websockets-17.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5aefe78e6a3077fe22b5e64b04666a85a3eb8b934d40e8595a693adcbceb6f11"}, + {file = "websockets-17.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f64e001bb7fa89b9f32cfa600bf8e9ac8ca26759d9b92ae01453ee303d9cd7b4"}, + {file = "websockets-17.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:677014a073bcb1fbaa7e21144786864f16c08f856d66834f611eceb9006cbab8"}, + {file = "websockets-17.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0de501b7f2db11e83739ac20e2d33d46da4604b829f506c24be80e7def069391"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f62114a54117e4948a1e414e89521f7fe1e3c2f83f2a571a06a4fc6718b0900a"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eec113a5b41d124ef42ff56b0d74a6da3fd986400038eab9e58ee42a4024e837"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5f051f8030a51815dc00e24bd2e5f1435af095c1cc111d747ac6e2a3620d7641"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:655a8e28010f09fd6fa317e857afab3af7647f33e41dee88fa421e92086d1090"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dc2b79afc074d2f3e64b26539350f697fe1b85ea1c49ea24eb588f247b053ce1"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e4bd7eacb87d8cf3ed70d6392c770a0d92441f05d7d2a3efafb5bc171d5e3067"}, + {file = "websockets-17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ccbf3f4a9890d50b3a08ee04029fde30a03bfdeffaa19977628bf17251764e60"}, + {file = "websockets-17.1-cp312-cp312-win32.whl", hash = "sha256:7e724f843fa6a0614aece65a7c73e51d0f4412ca41dccac13c3caf98e69536bb"}, + {file = "websockets-17.1-cp312-cp312-win_amd64.whl", hash = "sha256:617243e19a0992095956f406ee9cd3bc4ba92862d83cb1d83bb59ce574412bec"}, + {file = "websockets-17.1-cp312-cp312-win_arm64.whl", hash = "sha256:9f4a08ff7cb68c27b18e09223cc6304e01d0f82d5a240d251266dfd2e6e44729"}, + {file = "websockets-17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a0162a6372110a5601cb5c9fd826635cedf69f3e110c545dd19774e040b970e"}, + {file = "websockets-17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:829dba1bc049779de9b332088c1a6a9858e96bd67e50b6b644a95e02b67836bc"}, + {file = "websockets-17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd8f47dbf2e8adb15c847215f83436de3fdb120b51fdae0fbbdf69fd97a3ad80"}, + {file = "websockets-17.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9f4c0377a83e163a303514fdfab501dbe379bdc13e5b9312a91d112658b29dce"}, + {file = "websockets-17.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c3241d684a76eaaef8b2dc789afde4343cd3aad55ea81e4e8ab3605b529bae51"}, + {file = "websockets-17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5f5c7a893507d0e83a80b88aefd6522f7e882cd53f9722c6f23f5a020c9557c"}, + {file = "websockets-17.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00bf34b64501e3477e81fc281532ff3cbf4da26633c10b63979d5085d46602d3"}, + {file = "websockets-17.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ce0305b702b20d1e1d60a9aaace6bc89970e1753565543f310d549eab22c2435"}, + {file = "websockets-17.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29176d8b429cfa0fa443c473878d37a5c06cfd0cb36b71ba4314accc71e05906"}, + {file = "websockets-17.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3709a1ab30b4b922027d22f68d2b61a0656a91680ac894a537624e6be7dd7f7c"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43bd0c1ceb924d67f5c1a5254d8361dd9d94246e6331a726064dfa2917880780"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:1fce0f43e0d41422e0b2cad6561e1970df22f212f4c7e884967df7cf591b031c"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4031152769179ab8dcdeafc7b0e58052a49117560a28671700b47b2c7b717aad"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a06f3b5085176763182449559e20391d7ce616a8972a9f7a33deda87ea6d4f3c"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:77b37cceca17291897c3c73bd30a7c7c7909593554b5da574ec852af83c1742a"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d8e83333385cac6030a5167fd18bf96cc6c58b914c308e683f05b0cf94bc8dd0"}, + {file = "websockets-17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:073c5c3f7e127041fa9d34a9e29ceefee8c3cafbd267ed2927318f425144380d"}, + {file = "websockets-17.1-cp313-cp313-win32.whl", hash = "sha256:2afb58c7ba48b329d56769f8dfd89f394efe587b65ef806bae810a484d6d3608"}, + {file = "websockets-17.1-cp313-cp313-win_amd64.whl", hash = "sha256:0340bbef6bfbe16da888b3983d666a4db4954ac3253c38f13bc7aba0c7db5a2f"}, + {file = "websockets-17.1-cp313-cp313-win_arm64.whl", hash = "sha256:7a72efa3bf4fa3a6669a54420a472ad056da3973d827f10e3a536da463f926c2"}, + {file = "websockets-17.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0c9982938980e086da59f70d05f9418cd143401a601a0faac10fa48f7bb1cd3e"}, + {file = "websockets-17.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:57b39dc8541cf7ed3f639da82bf7451060483967f9e733da1f8173e4095f0642"}, + {file = "websockets-17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:96abdecbaae746851b87c3a36cb4a661df93ca3d92f114270f79228bf1d00de6"}, + {file = "websockets-17.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9fc873e239c5abeb150bc24dbd1a7af23a9254526383ce0a077f5e20adbeb19"}, + {file = "websockets-17.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f42912fa9eb4cb7c7ec9fde9b3332ba339eb8a8811981043d4029599f3d950b"}, + {file = "websockets-17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f98bf378d7a5be047a044a1a27c987a8f355e10e3b5754617dbe756248cbc5ce"}, + {file = "websockets-17.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d334d11398086bb5559606cb42d51c013ea7c061c7db701521392373d3c087f5"}, + {file = "websockets-17.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c27336b1a0ac56569493e858497870347854372395f50483725f8cdacc5a45c"}, + {file = "websockets-17.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67258b00302a5aaf0b267771c7014b13429abd7ea17eebc4c55bd935ff101555"}, + {file = "websockets-17.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:455ffeea0879d313205df1e745e5883e1feb7f31ecd26be882f5f0babd3db04f"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f7233eaf441a345a5943a929fd4b5ea3278f11aed35a9ed0f3106b8cb3ca846a"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:c65da239a5ad553619804c1f9d65c1a0b3005381c6158ee14da2c7444cbd0c78"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fa1ffa08c81a4f809cdab6129f8e55bee4650b9d6d3461019dda73aacd146b6"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:406b8107943a43ef4649b1e0cb0cdc052bbf08fe1c8905a623c4af9586e5cebb"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4e8ffcb486c8490a34a4cef5e4409d8da5a1cb1681e5bf7d786ce5e84aa8540d"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fb88076df585b69c5761c387c0081aa87d7b9eb1b205a6535ca4777e25650d81"}, + {file = "websockets-17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5d4724255fb8398acd9e583b97eb2279cec20e0bd0f9a94bf75f6056ef9f13da"}, + {file = "websockets-17.1-cp314-cp314-win32.whl", hash = "sha256:be3f0129c5654517b2abf07dcb75bb1d9479759a4ccfb569e8293579e9fc029a"}, + {file = "websockets-17.1-cp314-cp314-win_amd64.whl", hash = "sha256:2a4dc6ef83f4559e0d05f313a375cb38f63c986096a9da99fe94fdd779d313e5"}, + {file = "websockets-17.1-cp314-cp314-win_arm64.whl", hash = "sha256:46c0331c9eaaf73a559f3a9e388466be0df96eb83d40f06f1ca6ab6613b35c82"}, + {file = "websockets-17.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d411ea5ca18ac1b12c0c94be88b60c18ca641ac43bcdfdf1c9f79d46cdbe1603"}, + {file = "websockets-17.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:07fa3e7c30e2c577928d359b56bf872a3e0cbcc15553eaa0907c1ee86344b56f"}, + {file = "websockets-17.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6de9acef07e3a78e9567fcd26c29011a4da8f050b13004bbf880a0fd82a6eea5"}, + {file = "websockets-17.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ea0ed9373b880115911d9d39634bccc95b8ce590c9c42e8589f5cacc3ef3cee2"}, + {file = "websockets-17.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50903d335bfda026c2fa11dd9aed09d8cbee0c451e3a85122a9acb041b7dc69b"}, + {file = "websockets-17.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a74531ce81af587f906ab42f194032388fcff8fc7938402e5917c9147a39441"}, + {file = "websockets-17.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8fbf28e639544503b7d1c96452a5e5e043e4108d89b1f3fa02910603622d19db"}, + {file = "websockets-17.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f612dc57f00c07cf4aa2673f7cbceabd654ad2457b7e639f061b794d6e11f9fd"}, + {file = "websockets-17.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c7ac77401227212dc6e849182feee50d57cf456ec6329ffd6979c94bb136c5c"}, + {file = "websockets-17.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32a2a68d989d6e5b74a9d5095415c51189ebae29fceb7cf2b64a1c0318a81256"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:aec00f018d34c67500ff0438dc314b40277be4a1b983cbacbf53ccf7db63e257"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0014eaff8ad5b3b43feda2279f9d34bf2eaae040720b9fbbb55944b10f40b14d"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:db9d7ee47f3ba531e278be539af39e2c7c7d28fb94897b6cd1120d63b0ef5922"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:ff3e2ba7a9f0a110b0555452e9b5a03a34e11662544e01beea15f144b48ba7b7"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6da17fc94bd270f5987b10bee113461ac36a36a98b0481ddcc98056e5a90001a"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e8dc3fa6d6b7ead3f9de57895f41b116a28787548e066365d9d90f7356bcaad2"}, + {file = "websockets-17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b65d5fe48219dc2d5e158de9e6514e75600f379cc7e37108d35f31764c155566"}, + {file = "websockets-17.1-cp314-cp314t-win32.whl", hash = "sha256:2cce251f3e2469b99b6802b55435bcdd07123b41870f54c87b336183af9d7e68"}, + {file = "websockets-17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f6c38cdcaf98a911d7acc25577f2f9e710f3a2fc2bde1563556784320196b51"}, + {file = "websockets-17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:d1e2f5fa2b6d01f0d85b4f223fea7ed1d504be282a02a81bd2be4817ef7a2f03"}, + {file = "websockets-17.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:88381602e379165b66244b2ebc29f9b23ea0851fbe63ae157f91ca324f072d6f"}, + {file = "websockets-17.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:88bc5138e53903a85c354e59df7ba73ce306f7b09724cef74dba121e60a88ce2"}, + {file = "websockets-17.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3546ef55b3a074494106508bc6505c73825970d2d9505f7bf53882b3e88b0d1e"}, + {file = "websockets-17.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9ae55d24241fc055f22aea3ac924559069848bd0ad4ea065fdd72d2194685fe8"}, + {file = "websockets-17.1-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7b349265fad6244013eecd99df8d83c12bf3013943e431f4fadd5bffc37db42"}, + {file = "websockets-17.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc5789e5ea182b77a38881383ada5347202a6c66f4857d054e075290e80b604b"}, + {file = "websockets-17.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce13c7d233239e739600a57d4a73c1192ad8259e655a4d55aa1a454242bc809d"}, + {file = "websockets-17.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1036189bd34b0bc1b10a4679321e2c7968af317efe6e8e4c1c5141c4254fb5bb"}, + {file = "websockets-17.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e78fd4b7b2c5086a38671c9c882c1e643385eccea360b5b1fda4a105e590087e"}, + {file = "websockets-17.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:46e7a10bf04318c7b0c0273791925ae5e1cbe4a11e34aa934d2ef27862058a80"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:33e45c7ea38428e740a7f233555d71df0b875cef7fc080acebc9654475e35335"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6e63c01803be425ff062b7f7fc201a74def1d49fc94a2410dd17375df75936e9"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:722ec21717eec6477bce582147a28acdfe034e604239466a6a95daedb863e774"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:e74e41f0ad12ff1e8983e349daef79d37cc8280c743ce9d134d6c74c18dab5d6"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:12fe8984a32dbfd084e0603f1a8d740c0180cb85b3174585c54a80d2455a8394"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:01dcb47deebc40b38fd4a493b9b9f4d0a704b7bec6f35e4d34085b329abce71a"}, + {file = "websockets-17.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f4c45ee2512d3757b5e6c67c5a34e435143f2ecb7df3324f9fd888688c45c0f4"}, + {file = "websockets-17.1-cp315-cp315-win32.whl", hash = "sha256:0f4f50dfe2cc810fc4e2de979b35e83bf8bb4bccdc6fe472d93762ea7b1d5927"}, + {file = "websockets-17.1-cp315-cp315-win_amd64.whl", hash = "sha256:4af784f3e436f65b355c117c6497320f2b5cf6a559295cb1c4c7338e335d45cc"}, + {file = "websockets-17.1-cp315-cp315-win_arm64.whl", hash = "sha256:d58159af7835fde09c462394293c0d7aaf8fb4557d8f8e5699f5e722ccae013d"}, + {file = "websockets-17.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:1a5cf4e7bbe3ca499e6a289206cb4fcb7444b09919e129bd517f57d5fa192c13"}, + {file = "websockets-17.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:416b4bc8789a1865a3ff643ec4ee073a5f52402d0dbeafd27b1798d5dd6b6a51"}, + {file = "websockets-17.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:259f45358c76d3b18489e3e80636cdbe807e05ecf1b10fdf1a779106d23d0c8e"}, + {file = "websockets-17.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9d01e8ede41fea4f5a847dad9d628355f74905f437a5b6856d67aa66d193800"}, + {file = "websockets-17.1-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a7b35181a14cbfcae163b4de545d22abfd07d06c2c41ca69cfcd99251d6888ab"}, + {file = "websockets-17.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a8e768a048c2220697477ce2e67e4345dc9f693d0ee6af53945b5e30227c6a7"}, + {file = "websockets-17.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:880069d21cc33a558dcf180924a546d1ecf8ada5be3e4e70acee87019d706a24"}, + {file = "websockets-17.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cec1bb8f22abccc8d20f8ca63df9be41600c26c190f4b97ee86c675fd4a863a6"}, + {file = "websockets-17.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f3a1d577e081667dda7f8e5b4796e6e32f9713c93e2a3d930669519840a3c623"}, + {file = "websockets-17.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc053f9e95a76213c5eb7ed95779f7daf0d2bf0e4e03073629ebfa43a033f151"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:bb0efe019480a1c93e168ce96479273aaebd672fc8c350d5eed1e507ababb1b8"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:615746b12b26a3fd4077bc6fbeb277a1c192a45dd57b531d07ad9ed5c52a9a7a"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:1a20136d61f9ca3a31493732762661fafc2c20e8861930214e21afc6a8a692a2"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:2786cbd273ab69c22612db8a41229ddf2c158060b17b5928884bf388d07887f3"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:b1c323fc3be1dc3f87f6c59458cb7d9e13dcbbf971d6c3f3e2bbaf58d3bfcdfe"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:12c8e2b25df59755954a04dfa09c990b96691025aaf7eafd19ed6da24b09c18d"}, + {file = "websockets-17.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f58f58b4b29bbea2a3635e2c56eff4a3adab011fe383802a9e542e31b97085fc"}, + {file = "websockets-17.1-cp315-cp315t-win32.whl", hash = "sha256:f78a3ffb1994304db2c0c4588e4d1a518079b557054fa3bb985a6f5e50ff49a3"}, + {file = "websockets-17.1-cp315-cp315t-win_amd64.whl", hash = "sha256:ad68c28a27246fed109a4409393d677b7e1388345cbbd2f5aee5c182d8506110"}, + {file = "websockets-17.1-cp315-cp315t-win_arm64.whl", hash = "sha256:e552e0037230ac16e5f568de7012041344d1b18c9feed30ec2891b8eba55af81"}, + {file = "websockets-17.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10ecb38ffc05e1841b619d99c725307a223ef9ad58e7b1ed33311d472dc43918"}, + {file = "websockets-17.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17aa424ab61620aad21b36b2240efc87b500cc496e7d0e999a5c2ae99395e886"}, + {file = "websockets-17.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:764cf7bfa149365f32b7a0fd9fed32debdac29dd06295d5635cde1745b446cd8"}, + {file = "websockets-17.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8d1b108bd8f5f6a8b90801f6db3b3858d5deca889acfdb8ac497bbb24e4b0edf"}, + {file = "websockets-17.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a62d8c424383c9dc769ff3672018df822603117e32686e567d452ed035b6fb2e"}, + {file = "websockets-17.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8196d217eeca52b9235ee1f8a684a09885a5f953d5a31e80ef915bf2c5c94f9d"}, + {file = "websockets-17.1-py3-none-any.whl", hash = "sha256:f221081107b8c48184d99f7019604486376e7ef826037e70aad6b02540732c23"}, + {file = "websockets-17.1.tar.gz", hash = "sha256:acfea4c20bf54384883ea33b1240fc1db4f52e190823a4e2b334bc3e8bfca96a"}, +] + +[[package]] +name = "yarl" +version = "1.24.5" +description = "Yet another URL library" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88f50c94e21a0a7f14042c015b0eba1881af78562e7bf007e0033e624da59750"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6efbccc3d7f75d5b03105172a8dc86d82ba4da86817952529dd93185f4a88be2"}, + {file = "yarl-1.24.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0ebfaffe1a16cb72141c8e09f18cc76856dbe58639f393a4f2b26e474b96b871"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ac73abdc7ab75610f95a8fd994c6457e87752b02a63987e188f937a1fc180f0"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d97a951a81039050e45f04e96689b58b8243fa5e62aa14fe67cb6075300885e"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe7b7bb170daccbba19ad33012d2b15f1e7942296fd4d45fc1b79013da8cc0f2"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:89a1bbb58e0e3f7a283653d854b1e95d65e5cfd4af224dac5f02629ec1a3e621"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fa5e51397466ea7e98de493fa2ff1b8193cfef8a7b0f9b4842f92d342df0dba"}, + {file = "yarl-1.24.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4103b77b8a8225e413107d2349b65eb3c1c52627b5cc5c3c4c1c6a798b218950"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9f3e9c8a9ecffa57bef8fb4fa19e5fa4d2d8307cf6bac5b1fca5e5860f4ba00"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0ebc836c47a6477e182169c6a476fc691d12b518894bf7dd2572f0d59f1c7ed"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:96d30286dd02679e32a39aa8f0b7498fc847fcda46cfc09df5513e82ce252440"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:fd8c81f346b58f45818d09ea11db69a8d5fd34a224b79871f6d44f12cd7977b1"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5c55256dee8f4b27bfbf636c8363383c7c8db7890c7cba5217d7bd5f5f21dab6"}, + {file = "yarl-1.24.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9f4d8cf085a4c6a40fb97ea0f46938a8df43c85d31f9d45e2a8867ea9293790d"}, + {file = "yarl-1.24.5-cp310-cp310-win_amd64.whl", hash = "sha256:240cbec09667c1fed4c6cd0060b9ec57332427d7441289a2ed8875dc9fb2b224"}, + {file = "yarl-1.24.5-cp310-cp310-win_arm64.whl", hash = "sha256:8a6987eaad834cb32dd57d9d582225f0054a5d1af706ccfbbdba735af4927e13"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a"}, + {file = "yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e"}, + {file = "yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077"}, + {file = "yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd"}, + {file = "yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25"}, + {file = "yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec"}, + {file = "yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9"}, + {file = "yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce"}, + {file = "yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba"}, + {file = "yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b"}, + {file = "yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb"}, + {file = "yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16"}, + {file = "yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144"}, + {file = "yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4"}, + {file = "yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740"}, + {file = "yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d"}, + {file = "yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9"}, + {file = "yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5"}, + {file = "yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4"}, + {file = "yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad"}, + {file = "yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba"}, + {file = "yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a"}, + {file = "yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6"}, + {file = "yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047"}, + {file = "yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104"}, + {file = "yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688"}, + {file = "yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7"}, + {file = "yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f"}, ] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[metadata] +lock-version = "2.1" +python-versions = "^3.12" +content-hash = "b04ace60c79942d0057ba54af1f49a8078da05696a30caf80353269a980b84c0" diff --git a/pyproject.toml b/pyproject.toml index 478e095d..a08b4f0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "uniswap-python" -version = "0.4.5" -description = "An unofficial Python wrapper for the Uniswap exchange" +version = "0.9.0" # 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" keywords = ["uniswap", "ethereum", "exchange", "trading"] @@ -19,19 +19,46 @@ classifiers = [ ] +[tool.poetry.scripts] +unipy = "uniswap:main" + [tool.poetry.dependencies] -python = "^3.7" -web3 = "^5.12.0" +python = "^3.12" +web3 = "^8.0.0" +cytoolz = ">=0.12.3" +frozenlist = ">=1.5.0" +multidict = ">=6.0.5" +yarl = ">=1.10.0" +click = "^8" +python-dotenv = "*" +aiohttp = ">=3.9.0" +typing-extensions = ">=4.12.0" +# eth-keyfile (via web3->eth-account) imports pkg_resources, which setuptools +# dropped in 82.0.0 (last shipped in 81.0.0). Pin below that until web3 7.x +# (which pulls in a pkg_resources-free eth-keyfile) is adopted. +setuptools = "<82" [tool.poetry.dev-dependencies] -pytest = "^5.4.3" -black = "^19.10b0" -pytest-cov = "^2.10.0" -pytest-dotenv = "^0.5.2" -python-dotenv = "^0.14.0" -flake8 = "^3.8.3" -mypy = "^0.782" +mypy = "*" +pre-commit = "*" +pytest = "^8.4" +pytest-cov = "*" +pytest-dotenv = "*" +pytest-asyncio = "^1.4" + +ruff = ">=0.9" +Sphinx = "^9" +sphinx-book-theme = "^1.1" +sphinx-click = "*" +pydata-sphinx-theme = "*" + +[tool.pytest.ini_options] +log_cli = false # to print logs during tests, set to true +#log_level = "NOTSET" + +[tool.ruff.lint] +ignore = ["E402", "E501"] [build-system] -requires = ["poetry>=0.12"] -build-backend = "poetry.masonry.api" +requires = ["poetry-core>=1.0"] +build-backend = "poetry.core.masonry.api" diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index b63bc4a8..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,30 +0,0 @@ -atomicwrites==1.2.1 -attrdict==2.0.0 -attrs==18.2.0 -certifi==2018.11.29 -chardet==3.0.4 -cytoolz==0.9.0.1 -eth-abi==1.3.0 -eth-account==0.3.0 -eth-hash==0.2.0 -eth-keyfile==0.5.1 -eth-keys==0.2.0b3 -eth-rlp==0.1.2 -eth-typing==2.0.0 -eth-utils==1.4.1 -hexbytes==0.1.0 -idna==2.8 -lru-dict==1.1.6 -more-itertools==5.0.0 -parsimonious==0.8.1 -pluggy==0.8.0 -py==1.7.0 -pycryptodome==3.7.2 -pytest==4.0.2 -requests==2.21.0 -rlp==1.0.3 -six==1.12.0 -toolz==0.9.0 -urllib3==1.24.2 -web3==4.8.2 -websockets==6.0 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e2917cf1..00000000 --- a/requirements.txt +++ /dev/null @@ -1,43 +0,0 @@ -appdirs==1.4.3 -atomicwrites==1.2.1 -attrdict==2.0.0 -attrs==18.2.0 -black==18.9b0 -bleach==3.1.4 -certifi==2018.11.29 -chardet==3.0.4 -Click==7.0 -cytoolz==0.9.0.1 -docutils==0.14 -eth-abi==1.3.0 -eth-account==0.3.0 -eth-hash==0.2.0 -eth-keyfile==0.5.1 -eth-keys==0.2.0b3 -eth-rlp==0.1.2 -eth-typing==2.0.0 -eth-utils==1.4.1 -hexbytes==0.1.0 -idna==2.8 -lru-dict==1.1.6 -more-itertools==5.0.0 -parsimonious==0.8.1 -pkginfo==1.4.2 -pluggy==0.8.0 -py==1.7.0 -pycryptodome==3.7.2 -Pygments==2.3.1 -pytest==4.0.2 -readme-renderer==24.0 -requests==2.21.0 -requests-toolbelt==0.8.0 -rlp==1.0.3 -six==1.12.0 -toml==0.10.0 -toolz==0.9.0 -tqdm==4.28.1 -twine==1.12.1 -urllib3==1.24.2 -web3==4.8.2 -webencodings==0.5.1 -websockets==6.0 diff --git a/scripts/adoption_snapshot.py b/scripts/adoption_snapshot.py new file mode 100644 index 00000000..99262ad3 --- /dev/null +++ b/scripts/adoption_snapshot.py @@ -0,0 +1,750 @@ +#!/usr/bin/env python3 +""" +adoption_snapshot.py — Uniswap-Python Adoption Metrics + +Usage: + python3 scripts/adoption_snapshot.py [--out DIR] + python3 scripts/adoption_snapshot.py --report BASELINE.json + +Modes: + Snapshot (default): Emits a dated JSON + Markdown snapshot of: + - GitHub stars, forks, watchers, open issues + - GitHub Traffic: clones + unique visitors (14-day window — run frequently!) + - PyPI downloads last day/week/month (pypistats.org API) + - Recent release versions and dates + + Report (--report BASELINE.json): reads current metrics + a saved baseline + and prints a D4 table. Stars/forks/watchers are true cumulative counters + (delta is current − baseline). GitHub traffic is a rolling 14-day window — + those totals are NEVER subtracted. Period clone/view counts are + reconstructed by unioning the daily series stored in each snapshot. + The current UTC day is excluded from period totals — its Traffic row is + still accumulating (the 06:00 UTC scheduled run would otherwise undercount). + +GitHub Traffic cannot be read with GitHub Actions' default GITHUB_TOKEN +(it 403s even with contents: write). Use a PAT: + + Fine-grained: Repository permissions → Administration: Read, Contents: Read + Classic: public_repo (or repo) + + ADOPTION_GITHUB_TOKEN=github_pat_... python3 scripts/adoption_snapshot.py + # GITHUB_TOKEN is also accepted for local/manual runs + +Without a PAT, traffic metrics are omitted (other metrics still work). +The daily workflow reads repo secret ADOPTION_GITHUB_TOKEN and runs --strict +so a failed collector does not get committed as a successful snapshot. + +Version-split download counts require BigQuery — not yet wired; see TODO below. +""" +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +import urllib.error +import urllib.request +from datetime import date, datetime, timedelta, timezone +from pathlib import Path + +REPO = "uniswap-python/uniswap-python" +PACKAGE = "uniswap-python" +USER_AGENT = f"{PACKAGE}-adoption-tracker/1.0; +https://github.com/{REPO}" + +# Actions GITHUB_TOKEN always 403s on /traffic/* — needs a PAT instead. +_ACTIONS_TOKEN_PREFIXES = ("ghs_",) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _request_json( + url: str, + headers: dict | None = None, + retries: int = 5, +) -> tuple[dict | list | None, str | None]: + req_headers = {"User-Agent": USER_AGENT} + if headers: + req_headers.update(headers) + last_err: str | None = None + for attempt in range(retries): + req = urllib.request.Request(url, headers=req_headers) + try: + with urllib.request.urlopen(req, timeout=15) as r: + return json.loads(r.read()), None + except urllib.error.HTTPError as e: + last_err = f"HTTP {e.code}: {e.reason}" + retryable = e.code in (429, 500, 502, 503, 504) and attempt < retries - 1 + if retryable: + retry_after = e.headers.get("Retry-After") if e.headers else None + try: + delay = min(int(retry_after), 30) if retry_after else 2 ** (attempt + 1) + except (TypeError, ValueError): + delay = 2 ** (attempt + 1) + # pypistats.org 429s with no Retry-After; give it a couple of seconds. + time.sleep(max(delay, 2)) + continue + return None, last_err + except Exception as e: + return None, str(e) + return None, last_err + + +def _gh(path: str, token: str | None = None) -> tuple[dict | list | None, str | None]: + base = f"https://api.github.com/repos/{REPO}" + url = f"{base}/{path}" if path else base + headers = { + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + } + if token: + headers["Authorization"] = f"Bearer {token}" + return _request_json(url, headers) + + +def _fetch_json(url: str, headers: dict | None = None) -> tuple[dict | None, str | None]: + data, err = _request_json(url, headers) + if isinstance(data, list): + return None, "expected JSON object, got list" + return data, err + + +def _is_actions_token(token: str) -> bool: + return token.startswith(_ACTIONS_TOKEN_PREFIXES) + + +def _traffic_daily_series(payload: dict | None, series_key: str) -> list[dict]: + """Normalize GitHub Traffic ``clones``/``views`` arrays to ``[{date, count, uniques}]``.""" + if not isinstance(payload, dict): + return [] + rows: list[dict] = [] + for item in payload.get(series_key) or []: + if not isinstance(item, dict): + continue + day = str(item.get("timestamp") or "")[:10] + if len(day) != 10: + continue + rows.append( + { + "date": day, + "count": item.get("count"), + "uniques": item.get("uniques"), + } + ) + return rows + + +# --------------------------------------------------------------------------- +# Collectors +# --------------------------------------------------------------------------- + + +def _collect_github(token: str | None) -> dict: + data, err = _gh("", token) + if err: + return {"error": err} + return { + "stars": data.get("stargazers_count"), + "forks": data.get("forks_count"), + "watchers": data.get("subscribers_count"), + "open_issues": data.get("open_issues_count"), + } + + +def _collect_traffic(token: str | None) -> dict: + if not token: + return { + "note": ( + "Skipped — set ADOPTION_GITHUB_TOKEN (PAT with Administration:read) " + "to capture traffic (14-day window only). The default Actions " + "GITHUB_TOKEN cannot read /traffic/*." + ) + } + if _is_actions_token(token): + return { + "error": ( + "GitHub Actions GITHUB_TOKEN cannot access the Traffic API " + "(403 even with contents: write). Set repo secret " + "ADOPTION_GITHUB_TOKEN to a PAT with Administration:read " + "(fine-grained) or public_repo (classic)." + ) + } + + # per=day keeps a reconstructable series. per=week would drop daily + # resolution and make period totals over >14 days impossible. + clones, c_err = _gh("traffic/clones?per=day", token) + views, v_err = _gh("traffic/views?per=day", token) + clones_d = clones if isinstance(clones, dict) else None + views_d = views if isinstance(views, dict) else None + result: dict = { + "clones_14d": clones_d.get("count") if clones_d else None, + "unique_cloners_14d": clones_d.get("uniques") if clones_d else None, + "views_14d": views_d.get("count") if views_d else None, + "unique_visitors_14d": views_d.get("uniques") if views_d else None, + "clones_daily": _traffic_daily_series(clones_d, "clones"), + "views_daily": _traffic_daily_series(views_d, "views"), + } + if c_err: + result["clones_error"] = c_err + if v_err: + result["views_error"] = v_err + return result + + +def _collect_pypi_recent() -> dict: + # TODO: version-split downloads require BigQuery or a scraper not yet wired. + # pypistats.org /recent gives overall day/week/month totals only. + url = f"https://pypistats.org/api/packages/{PACKAGE}/recent" + data, err = _fetch_json(url) + if err: + result = {"error": err} + if "429" in err: + result["note"] = "pypistats.org rate-limited; retry after a few minutes" + return result + d = data.get("data", {}) + return { + "downloads_last_day": d.get("last_day"), + "downloads_last_week": d.get("last_week"), + "downloads_last_month": d.get("last_month"), + "version_split_note": ( + "Per-version counts require BigQuery (pypistats.org does not expose them). " + "To check v4 pickup specifically, compare release-date-filtered BigQuery results." + ), + } + + +def _collect_pypi_versions() -> list[dict]: + """Top-10 versions by recency from PyPI JSON API.""" + url = f"https://pypi.org/pypi/{PACKAGE}/json" + data, err = _fetch_json(url) + if err: + return [{"error": err}] + releases = data.get("releases", {}) + versions = [] + for ver, files in releases.items(): + if not files: + continue + upload_time = sorted(f.get("upload_time", "") for f in files)[-1] + versions.append({"version": ver, "released": upload_time}) + versions.sort(key=lambda x: x["released"], reverse=True) + return versions[:10] + + +# --------------------------------------------------------------------------- +# Snapshot +# --------------------------------------------------------------------------- + + +def collect_metrics(token: str | None = None, *, now: datetime | None = None) -> dict: + collected_at = now or datetime.now(timezone.utc) + return { + "snapshot_at": collected_at.isoformat(), + "repo": REPO, + "package": PACKAGE, + "github": _collect_github(token), + "github_traffic": _collect_traffic(token), + "pypi": _collect_pypi_recent(), + "pypi_versions": _collect_pypi_versions(), + } + + +def collection_errors(metrics: dict, *, require_traffic: bool = False) -> list[str]: + """Return human-readable collector failures (empty if the snapshot is complete). + + ``require_traffic=True`` (``--strict`` / GitHub Actions) also treats a + skipped or empty traffic section as a failure so the daily workflow cannot + commit N/A clones/visitors as a successful snapshot. + """ + errors: list[str] = [] + gh = metrics.get("github") or {} + if gh.get("error"): + errors.append(f"github: {gh['error']}") + traffic = metrics.get("github_traffic") or {} + for key in ("error", "clones_error", "views_error"): + if traffic.get(key): + errors.append(f"github_traffic.{key}: {traffic[key]}") + if require_traffic: + if traffic.get("note"): + errors.append(f"github_traffic: {traffic['note']}") + elif traffic.get("clones_14d") is None or traffic.get("views_14d") is None: + errors.append("github_traffic: clones/views missing from snapshot") + elif not traffic.get("clones_daily") or not traffic.get("views_daily"): + errors.append( + "github_traffic: daily series missing " + "(need per=day clones/views to reconstruct period totals)" + ) + pypi = metrics.get("pypi") or {} + if pypi.get("error"): + errors.append(f"pypi: {pypi['error']}") + versions = metrics.get("pypi_versions") or [] + if versions and isinstance(versions[0], dict) and versions[0].get("error"): + errors.append(f"pypi_versions: {versions[0]['error']}") + return errors + + +# --------------------------------------------------------------------------- +# Period reconstruction (GitHub traffic is a rolling 14-day window) +# --------------------------------------------------------------------------- + + +def _snapshot_day(snapshot: dict) -> str: + return str(snapshot.get("snapshot_at") or "")[:10] + + +def _days_in_range(start: str, end: str) -> list[str]: + try: + s = date.fromisoformat(start) + e = date.fromisoformat(end) + except ValueError: + return [] + if e < s: + return [] + days: list[str] = [] + d = s + while d <= e: + days.append(d.isoformat()) + d += timedelta(days=1) + return days + + +def load_snapshots(metrics_dir: Path) -> list[dict]: + """Load ``adoption-YYYY-MM-DD.json`` files; skip unreadable ones.""" + snapshots: list[dict] = [] + if not metrics_dir.is_dir(): + return snapshots + for path in sorted(metrics_dir.glob("adoption-*.json")): + try: + data = json.loads(path.read_text()) + except (OSError, json.JSONDecodeError): + continue + if isinstance(data, dict): + snapshots.append(data) + return snapshots + + +def complete_period_end(end: str, *, now: date | None = None) -> tuple[str, str | None]: + """Inclusive period end with the collection-day dropped if still open. + + GitHub Traffic rows for the snapshot's UTC day are still accumulating + at collection time. Default ``today`` to that snapshot day — do **not** + re-read the clock. Collection that starts before UTC midnight and + finishes after would otherwise see ``end != datetime.now().date()`` and + publish the still-partial collection-day row as a complete-period total. + + Pass ``now`` to treat a historical snapshot as complete (``end < now``). + Historical ends are unchanged. + + Returns ``(closed_end, excluded_day_or_None)``. + """ + try: + end_day = date.fromisoformat(end) + except ValueError: + return end, None + today = now if now is not None else end_day + today_s = today.isoformat() + if end == today_s: + closed = (today - timedelta(days=1)).isoformat() + return closed, today_s + return end, None + + +def reconstruct_period_counts( + snapshots: list[dict], + start_date: str, + end_date: str, + series_field: str, +) -> dict: + """Union daily clone/view *counts* across snapshots for ``[start, end]``. + + Same-day rows take the max count (later snapshots complete "today"). + Unique counts are not reconstructed — they are not additive across days. + Callers that want a closed period should pass ``complete_period_end()``. + """ + by_day: dict[str, int] = {} + for snap in snapshots: + traffic = snap.get("github_traffic") or {} + for row in traffic.get(series_field) or []: + if not isinstance(row, dict): + continue + day = str(row.get("date") or "")[:10] + if len(day) != 10 or not (start_date <= day <= end_date): + continue + try: + n = int(row["count"]) + except (KeyError, TypeError, ValueError): + continue + prev = by_day.get(day) + by_day[day] = n if prev is None else max(prev, n) + + span_days = _days_in_range(start_date, end_date) + missing = [d for d in span_days if d not in by_day] + return { + "total": sum(by_day.values()) if by_day else None, + "days_covered": len(by_day), + "days_span": len(span_days), + "missing_days": missing, + } + + +# --------------------------------------------------------------------------- +# Formatters +# --------------------------------------------------------------------------- + + +def _md_table(headers: list[str], rows: list[list]) -> str: + def fmt(v): + return "N/A" if v is None else str(v) + + lines = ["| " + " | ".join(headers) + " |", "|" + " --- |" * len(headers)] + for row in rows: + lines.append("| " + " | ".join(fmt(v) for v in row) + " |") + return "\n".join(lines) + + +def format_markdown(m: dict) -> str: + gh = m.get("github", {}) + traffic = m.get("github_traffic", {}) + pypi = m.get("pypi", {}) + versions = m.get("pypi_versions", []) + + sections = [ + f"# uniswap-python Adoption Snapshot", + f"", + f"**Snapshot date**: {m['snapshot_at']}", + f"", + f"## GitHub", + f"", + _md_table( + ["Metric", "Value"], + [ + ["Stars", gh.get("stars", "N/A")], + ["Forks", gh.get("forks", "N/A")], + ["Watchers", gh.get("watchers", "N/A")], + ["Open Issues", gh.get("open_issues", "N/A")], + ], + ), + f"", + f"## GitHub Traffic (14-day window)", + f"", + ] + + traffic_err = traffic.get("error") or traffic.get("clones_error") or traffic.get("views_error") + if "note" in traffic and not traffic_err: + sections += [f"_{traffic['note']}_", ""] + elif traffic_err: + sections += [f"_Error: {traffic_err}_", ""] + else: + sections += [ + _md_table( + ["Metric", "Value"], + [ + ["Clones (14d)", traffic.get("clones_14d", "N/A")], + ["Unique Cloners (14d)", traffic.get("unique_cloners_14d", "N/A")], + ["Views (14d)", traffic.get("views_14d", "N/A")], + ["Unique Visitors (14d)", traffic.get("unique_visitors_14d", "N/A")], + [ + "Daily series stored", + f"{len(traffic.get('clones_daily') or [])} clone-days / " + f"{len(traffic.get('views_daily') or [])} view-days", + ], + ], + ), + "", + ] + + sections += [ + f"## PyPI Downloads", + f"", + _md_table( + ["Period", "Downloads"], + [ + ["Last day", pypi.get("downloads_last_day", "N/A")], + ["Last week", pypi.get("downloads_last_week", "N/A")], + ["Last month", pypi.get("downloads_last_month", "N/A")], + ], + ), + f"", + f"_{pypi.get('version_split_note') or pypi.get('note', '')}_", + f"", + f"## Recent Releases", + f"", + _md_table( + ["Version", "Released"], + [[v.get("version", "?"), v.get("released", "?")[:10]] for v in versions[:5]], + ), + f"", + ] + + return "\n".join(sections) + + +def _delta(old, new) -> str: + if old is None or new is None: + return "N/A" + try: + d = int(new) - int(old) + return f"+{d}" if d >= 0 else str(d) + except (TypeError, ValueError): + return "N/A" + + +def _format_traffic_report( + baseline: dict, + current: dict, + snapshots: list[dict] | None, + *, + now: date | None = None, +) -> list[str]: + """Traffic section: reconstruct period counts; never subtract 14d windows.""" + start = _snapshot_day(baseline) + raw_end = _snapshot_day(current) + end, partial_day = complete_period_end(raw_end, now=now) + all_snaps = list(snapshots or []) + all_snaps.extend([baseline, current]) + + clones = reconstruct_period_counts(all_snaps, start, end, "clones_daily") + views = reconstruct_period_counts(all_snaps, start, end, "views_daily") + bt = baseline.get("github_traffic") or {} + ct = current.get("github_traffic") or {} + + lines = [ + "## GitHub Traffic", + "", + "GitHub's Traffic API retains **14 days**. The `*_14d` fields are rolling", + "windows, not cumulative counters — they are **not subtracted** (a later", + "window minus an earlier one is not period traffic).", + "Period clone/view totals are reconstructed by unioning the daily series", + "stored in each snapshot. The current UTC day is excluded — its row is", + "still accumulating.", + "", + ] + + empty_complete_window = bool(partial_day) and end < start + no_series = clones["days_covered"] == 0 and views["days_covered"] == 0 + + if empty_complete_window: + lines += [ + f"_No complete UTC days in {start} → {raw_end} yet. " + f"{partial_day} is still accumulating and is excluded from " + f"period totals._", + "", + ] + elif no_series: + lines += [ + "_Period totals unavailable — snapshots do not yet contain a daily " + "traffic series. After the PAT secret is set, each daily run stores " + "the 14-day breakdown so D4 can reconstruct the grant window._", + "", + ] + else: + lines += [ + f"### Period totals ({start} → {end})", + "", + _md_table( + ["Metric", "Total", "Coverage"], + [ + [ + "Clones", + clones["total"], + f"{clones['days_covered']}/{clones['days_span']} days", + ], + [ + "Views", + views["total"], + f"{views['days_covered']}/{views['days_span']} days", + ], + ], + ), + "", + ] + if partial_day: + lines += [ + f"_{partial_day} excluded — GitHub Traffic for the current " + f"UTC day is still accumulating._", + "", + ] + missing = sorted(set(clones["missing_days"] + views["missing_days"])) + if missing: + shown = missing[:12] + extra = f" (+{len(missing) - 12} more)" if len(missing) > 12 else "" + lines += [f"_Gaps: {', '.join(shown)}{extra}_", ""] + lines += [ + "Unique cloners/visitors are **not additive** across days and cannot " + "be reconstructed as a period total.", + "", + ] + + lines += [ + "### Rolling 14-day windows (point-in-time, not a period delta)", + "", + _md_table( + ["Metric", "Baseline (14d)", "Current (14d)"], + [ + ["Clones", bt.get("clones_14d", "N/A"), ct.get("clones_14d", "N/A")], + [ + "Unique cloners", + bt.get("unique_cloners_14d", "N/A"), + ct.get("unique_cloners_14d", "N/A"), + ], + ["Views", bt.get("views_14d", "N/A"), ct.get("views_14d", "N/A")], + [ + "Unique visitors", + bt.get("unique_visitors_14d", "N/A"), + ct.get("unique_visitors_14d", "N/A"), + ], + ], + ), + "", + ] + return lines + + +def format_report( + baseline: dict, + current: dict, + snapshots: list[dict] | None = None, + *, + now: date | None = None, +) -> str: + bg = baseline.get("github", {}) + cg = current.get("github", {}) + bp = baseline.get("pypi", {}) + cp = current.get("pypi", {}) + + sections = [ + "# uniswap-python Adoption Report — Deltas", + "", + f"Baseline: {baseline['snapshot_at']}", + f"Current: {current['snapshot_at']}", + "", + "## GitHub", + "", + _md_table( + ["Metric", "Baseline", "Current", "Delta"], + [ + ["Stars", bg.get("stars", "N/A"), cg.get("stars", "N/A"), _delta(bg.get("stars"), cg.get("stars"))], + ["Forks", bg.get("forks", "N/A"), cg.get("forks", "N/A"), _delta(bg.get("forks"), cg.get("forks"))], + ["Watchers", bg.get("watchers", "N/A"), cg.get("watchers", "N/A"), _delta(bg.get("watchers"), cg.get("watchers"))], + ], + ), + "", + *_format_traffic_report(baseline, current, snapshots, now=now), + "## PyPI Downloads", + "", + "_PyPI day/week/month figures are also rolling windows; deltas below " + "compare two windows, not downloads accumulated over the grant period._", + "", + _md_table( + ["Period", "Baseline", "Current", "Window delta"], + [ + ["Last day", bp.get("downloads_last_day", "N/A"), cp.get("downloads_last_day", "N/A"), + _delta(bp.get("downloads_last_day"), cp.get("downloads_last_day"))], + ["Last week", bp.get("downloads_last_week", "N/A"), cp.get("downloads_last_week", "N/A"), + _delta(bp.get("downloads_last_week"), cp.get("downloads_last_week"))], + ["Last month", bp.get("downloads_last_month", "N/A"), cp.get("downloads_last_month", "N/A"), + _delta(bp.get("downloads_last_month"), cp.get("downloads_last_month"))], + ], + ), + "", + ] + + return "\n".join(sections) + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def _token_from_env() -> str | None: + return os.environ.get("ADOPTION_GITHUB_TOKEN") or os.environ.get("GITHUB_TOKEN") or None + + +def main() -> None: + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("--out", default="metrics", help="Output directory (default: metrics/)") + parser.add_argument( + "--report", + metavar="BASELINE.json", + help="Report mode: compute deltas vs baseline snapshot", + ) + parser.add_argument( + "--metrics-dir", + default=None, + help="Directory of adoption-*.json snapshots used to reconstruct " + "period traffic (default: directory of --report)", + ) + parser.add_argument( + "--strict", + action="store_true", + help="Exit 1 if any collector failed (default in GitHub Actions)", + ) + parser.add_argument( + "--allow-partial", + action="store_true", + help="Write snapshot and exit 0 even if some collectors failed", + ) + args = parser.parse_args() + + token = _token_from_env() + in_actions = os.environ.get("GITHUB_ACTIONS") == "true" + strict = (args.strict or in_actions) and not args.allow_partial + # One clock for snapshot_at, filename, and period-end exclusion so a + # midnight boundary during network I/O cannot desynchronize them. + collected_at = datetime.now(timezone.utc) + + if args.report: + baseline_path = Path(args.report) + if not baseline_path.exists(): + print(f"ERROR: Baseline file not found: {baseline_path}", file=sys.stderr) + sys.exit(1) + baseline = json.loads(baseline_path.read_text()) + current = collect_metrics(token, now=collected_at) + metrics_dir = Path(args.metrics_dir) if args.metrics_dir else baseline_path.parent + snapshots = load_snapshots(metrics_dir) + print(format_report(baseline, current, snapshots, now=collected_at.date())) + errors = collection_errors(current, require_traffic=strict) + if errors: + print("Collector errors:", file=sys.stderr) + for err in errors: + print(f" - {err}", file=sys.stderr) + if strict: + sys.exit(1) + return + + metrics = collect_metrics(token, now=collected_at) + errors = collection_errors(metrics, require_traffic=strict) + + out_dir = Path(args.out) + out_dir.mkdir(parents=True, exist_ok=True) + + date_str = collected_at.strftime("%Y-%m-%d") + json_path = out_dir / f"adoption-{date_str}.json" + md_path = out_dir / f"adoption-{date_str}.md" + + json_path.write_text(json.dumps(metrics, indent=2) + "\n") + md_path.write_text(format_markdown(metrics)) + + print(f"Snapshot written:\n JSON: {json_path}\n Markdown: {md_path}\n") + + gh = metrics.get("github", {}) + pypi = metrics.get("pypi", {}) + print(f"Stars: {gh.get('stars', 'N/A')} Forks: {gh.get('forks', 'N/A')} " + f"PyPI/month: {pypi.get('downloads_last_month', 'N/A')}") + + if errors: + print("Collector errors:", file=sys.stderr) + for err in errors: + print(f" - {err}", file=sys.stderr) + if strict: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/tests/pool_list.test b/tests/pool_list.test new file mode 100644 index 00000000..70ae2865 --- /dev/null +++ b/tests/pool_list.test @@ -0,0 +1 @@ +0x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48500100x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2110x00000000000000000000000000000000000000000x18084fbA666a33d37592fA2633fD49a74DD93a880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB4827001000x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2300010x0747f544426621534891D1A619457D7fD7bfE0880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2300010x5E5bF8B6643e9C0E957b66B86D316926b41460880x00000000000000000000000000000000000000000xa250CC729Bb3323e7933022a67B52200fE3547670600x12b504160222D66c38d916d9FbA11B613c51E8880x1f9840a85d5aF5bf1D1762F925BDADdC4201F9840xE8E1F50392Bd61D0F8F48E8E7aF51D3b8a52090a0600xCF1E7189264a84D7454077dC713c3d11400de8880x2078f336Fdd260f708BEc4a20c82b063274E1b230x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C5990600x948922B055187c7366E71B876aB1242EBBAeA8880x80ac24aA929eaF5013f6436cdA2a7ba190f5Cc0b0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48500100x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xbdC7c08592Ee4aa51D06C27Ee23D5087D65aDbcD0600x45435A50305D839607C62c601Bb5181476B328880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xbdC7c08592Ee4aa51D06C27Ee23D5087D65aDbcD0600xaB5aD6575a090244a43d9E20f3dBF51Ec10768880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xbdC7c08592Ee4aa51D06C27Ee23D5087D65aDbcD0600x78fbc642253fE3dC0dFd0E865F1e1f8A378Ce8880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xbdC7c08592Ee4aa51D06C27Ee23D5087D65aDbcD0600xA7B55087B8908A59E2A353708aDb543Cdf6B68880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xbdC7c08592Ee4aa51D06C27Ee23D5087D65aDbcD0600x9D096f215C52d2820c4C123ae5533fb8574128880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xbdC7c08592Ee4aa51D06C27Ee23D5087D65aDbcD0600x2832d3Ef71944E75b749E8902F45dc5E47B028880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xbdC7c08592Ee4aa51D06C27Ee23D5087D65aDbcD0600xb135E857Ea8c11Fa535d6433a85928fE0EF4A8C80x558AFaF6FeF52395D558F9fc1ab18A08C7A7548b0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc210000600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x0B010000b7624eb9B3DfBC279673C76E9D29D5F7100002000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xcccCCCcCCC33D538DBC2EE4fEab0a7A1FF4e8A94100002000x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xdAC17F958D2ee523a2206206994597C13D831ec71010x8F892d3B512B5d8D542F9FbCC55589f7D8f508C00xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xdAC17F958D2ee523a2206206994597C13D831ec71010xC5f78930B92Dd7dc3DA2745d6974c228C8BE88c00x42000000000000000000000000000000000000060x833589fCD6eDb6E08f4c7C32D4f71b54bdA029133000600xd98c5385218452B81c97E8332b15597ABc0f08880x5f4eC3Df9cbd43714FE2740f5E3616155c5b84190xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483000600x0F3894aE6e93c4a0eee4afC14e78E104A6E188880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc23000600xc5142fAC0A46B53BFA348770952aC98D6f1Fc8880xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xdAC17F958D2ee523a2206206994597C13D831ec73000600xc5142fAC0A46B53BFA348770952aC98D6f1Fc8880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xc221b7E65FfC80DE234bbB6667aBDd46593D34F03000600x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xc221b7E65FfC80DE234bbB6667aBDd46593D34F03001600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x157a6df6B74F4E5E45af4E4615FDe7B49225a662100012000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x157a6df6B74F4E5E45af4E4615FDe7B49225a66299992000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x157a6df6B74F4E5E45af4E4615FDe7B49225a66299982000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x17906b1Cd88aA8EfaEfC5e82891B52a22219BD453001600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x17906b1Cd88aA8EfaEfC5e82891B52a22219BD453002600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483002600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xe6fd75ff38Adca4B97FBCD938c86b987724318673000600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x17906b1Cd88aA8EfaEfC5e82891B52a22219BD453000600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x17906b1Cd88aA8EfaEfC5e82891B52a22219BD453003600x00000000000000000000000000000000000000000x93185F12743E0276CdcC975CD99E1da29C894c1d0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc23000600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2010x57991106cb7Aa27E2771Beda0D6522F68524A8880x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca00xae7ab96520DE3A18E5e111B5EaAb095312D7fE84010xa88aaCF73DF2BcCFabCFd1e7B597185cac9F28880x00000000000000000000000000000000000000000x1f9840a85d5aF5bf1D1762F925BDADdC4201F9840327670xB13250f0Dc8ec6dE297E81CDA8142DB51860BaC40x00000000000000000000000000000000000000000xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D720327670xB13250f0Dc8ec6dE297E81CDA8142DB51860BaC40x00000000000000000000000000000000000000000x4FbB350052Bca5417566f188eB2EBCE5b19BC9640327670xB13250f0Dc8ec6dE297E81CDA8142DB51860BaC40x00000000000000000000000000000000000000000xE41d2489571d322189246DaFA5ebDe1F4699F4980327670xB13250f0Dc8ec6dE297E81CDA8142DB51860BaC40x8170578B5576c0c19C5c605b78Dd83AE898AC4590xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB4810010x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6033F7f88332B8db6ad452B7C6D5bB643990aE3f3005600x00000000000000000000000000000000000000000x8170578B5576c0c19C5c605b78Dd83AE898AC4590xFA1b1f13080857Bf373de0De93970c96d2C29FD0500100x00000000000000000000000000000000000000000x8170578B5576c0c19C5c605b78Dd83AE898AC4590xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48500100x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6033F7f88332B8db6ad452B7C6D5bB643990aE3f3004600x00000000000000000000000000000000000000000xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xdAC17F958D2ee523a2206206994597C13D831ec710000600xc5142fAC0A46B53BFA348770952aC98D6f1Fc8880xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xdAC17F958D2ee523a2206206994597C13D831ec711000600xc5142fAC0A46B53BFA348770952aC98D6f1Fc8880xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xdAC17F958D2ee523a2206206994597C13D831ec712000600xc5142fAC0A46B53BFA348770952aC98D6f1Fc8880xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xdAC17F958D2ee523a2206206994597C13D831ec713000600xc5142fAC0A46B53BFA348770952aC98D6f1Fc8880x00000000000000000000000000000000000000000x6033F7f88332B8db6ad452B7C6D5bB643990aE3f3000600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x58D97B57BB95320F9a05dC918Aef65434969c2B23005600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xcccCCCcCCC33D538DBC2EE4fEab0a7A1FF4e8A94100012000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xf94e7d0710709388bCe3161C32B4eEA56d3f91CC280000600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xf94e7d0710709388bCe3161C32B4eEA56d3f91CC165000600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xf94e7d0710709388bCe3161C32B4eEA56d3f91CC50000600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xf94e7d0710709388bCe3161C32B4eEA56d3f91CC72500600x00000000000000000000000000000000000000000xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xdAC17F958D2ee523a2206206994597C13D831ec715000600xc5142fAC0A46B53BFA348770952aC98D6f1Fc8880x70e32318B0C8e41F3aEe788120FA183cd510bDc10xdAC17F958D2ee523a2206206994597C13D831ec7500100x00000000000000000000000000000000000000000x70e32318B0C8e41F3aEe788120FA183cd510bDc10xdAC17F958D2ee523a2206206994597C13D831ec710010x00000000000000000000000000000000000000000x8170578B5576c0c19C5c605b78Dd83AE898AC4590xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483000600x00000000000000000000000000000000000000000x70e32318B0C8e41F3aEe788120FA183cd510bDc10xFA1b1f13080857Bf373de0De93970c96d2C29FD010010x00000000000000000000000000000000000000000x70e32318B0C8e41F3aEe788120FA183cd510bDc10xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48500100x00000000000000000000000000000000000000000x8170578B5576c0c19C5c605b78Dd83AE898AC4590xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48100002000x00000000000000000000000000000000000000000x70e32318B0C8e41F3aEe788120FA183cd510bDc10xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483000600x00000000000000000000000000000000000000000x70e32318B0C8e41F3aEe788120FA183cd510bDc10xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48100002000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x7159cc276D7d17Ab4b3bEb19959E1F39368a45Ba3005600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x58D97B57BB95320F9a05dC918Aef65434969c2B22999600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x58D97B57BB95320F9a05dC918Aef65434969c2B22998600x00000000000000000000000000000000000000000xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xdAC17F958D2ee523a2206206994597C13D831ec717000600xc5142fAC0A46B53BFA348770952aC98D6f1Fc8880xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xdAC17F958D2ee523a2206206994597C13D831ec714000600xc5142fAC0A46B53BFA348770952aC98D6f1Fc8880x00000000000000000000000000000000000000000x7159cc276D7d17Ab4b3bEb19959E1F39368a45Ba3000600x00000000000000000000000000000000000000000x80ac24aA929eaF5013f6436cdA2a7ba190f5Cc0b0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48501100x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48500001250x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48600001250x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2300010x23CE72f64743a067F2fFA0ba782190bAa4A4e0880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xd0D9C3DFCE6fC759c071eAcf7200876019285288500100x00000000000000000000000000000000000000000xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a0xE0f63A424a4439cBE457D80E4f4b51aD25b2c56C100002000x00000000000000000000000000000000000000000xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a0xE0f63A424a4439cBE457D80E4f4b51aD25b2c56C500001000x00000000000000000000000000000000000000000xaaeE1A9723aaDB7afA2810263653A34bA2C21C7a0xE0f63A424a4439cBE457D80E4f4b51aD25b2c56C102000x00000000000000000000000000000000000000000x0B010000b7624eb9B3DfBC279673C76E9D29D5F70xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483005600x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xb755506531786C8aC63B756BaB1ac387bACB0C043005600x00000000000000000000000000000000000000000x77146784315Ba81904d654466968e3a7c196d1f30xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483005600x00000000000000000000000000000000000000000x149aF500734056B98572b66E6C771E57408e12e40xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2100002000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x5C1d9aA868a30795F92fAe903eDc9eFF269044bf100012000x00000000000000000000000000000000000000000x80ac24aA929eaF5013f6436cdA2a7ba190f5Cc0b0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB4880150x00000000000000000000000000000000000000000x80ac24aA929eaF5013f6436cdA2a7ba190f5Cc0b0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB4880050x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xdAC17F958D2ee523a2206206994597C13D831ec7400100x9a1e1E02474A07B1e3d28D18d8027e75aC6f04120x00000000000000000000000000000000000000000xd9Fcd98c322942075A5C3860693e9f4f03AAE07b0327670xB13250f0Dc8ec6dE297E81CDA8142DB51860BaC40x00000000000000000000000000000000000000000x699Ccf919C1dfdFa4C374292f42CAdC9899BF753100011000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x699Ccf919C1dfdFa4C374292f42CAdC9899BF753100022000x00000000000000000000000000000000000000000x699Ccf919C1dfdFa4C374292f42CAdC9899BF7530xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48100022000x00000000000000000000000000000000000000000x699Ccf919C1dfdFa4C374292f42CAdC9899BF7530xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB4875001000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x699Ccf919C1dfdFa4C374292f42CAdC9899BF75375001000x00000000000000000000000000000000000000000x699Ccf919C1dfdFa4C374292f42CAdC9899BF7530xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB4875002000x00000000000000000000000000000000000000000x437cc33344a0B27A429f795ff6B469C72698B2910xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48010xAf53Cb78035A8E0acCe38441793E2648B15B88a00x00000000000000000000000000000000000000000xdAC17F958D2ee523a2206206994597C13D831ec70327670xB13250f0Dc8ec6dE297E81CDA8142DB51860BaC40x00000000000000000000000000000000000000000xbBcdC8eb044bF661EabFA07b93909a76ebdb1100100052000x00000000000000000000000000000000000000000x77146784315Ba81904d654466968e3a7c196d1f30xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48100051000x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xbBcdC8eb044bF661EabFA07b93909a76ebdb1100100001000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xbBcdC8eb044bF661EabFA07b93909a76ebdb1100100001000x00000000000000000000000000000000000000000x77146784315Ba81904d654466968e3a7c196d1f30xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48100001000x00000000000000000000000000000000000000000x77146784315Ba81904d654466968e3a7c196d1f30xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483000600x00000000000000000000000000000000000000000xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xd821363B90b012537C831672234a70271b4aF24D3000600xe7af6b65890825982C1B45778297A38d4Ee9B4F60xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xd821363B90b012537C831672234a70271b4aF24D3000600xD29aacDF56A878c09Bea26b87629168154a7b7fe0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xd821363B90b012537C831672234a70271b4aF24D1000100xD29aacDF56A878c09Bea26b87629168154a7b7fe0x00000000000000000000000000000000000000000x14cF922aa1512Adfc34409b63e18D391e4a86A2f10005500x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005500x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005300x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005700x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005800x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005900x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005950x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005930x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005920x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005890x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005880x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005780x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005770x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005760x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005750x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x6964224fAe4849f2255F784365F0737D48cdEe4810005740x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed10005550x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed10005600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed10005590x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed10005580x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed100002000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed100003000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed100002500x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed100003500x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed100004000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed100004500x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xd66021427082d3EE8A136D97aaDa19DB0eD323ed100005000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xE770c6d69e68dF8Bd1976459DD8705C26ed8115D100002000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xE770c6d69e68dF8Bd1976459DD8705C26ed8115D3000600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xE770c6d69e68dF8Bd1976459DD8705C26ed8115D100003000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xE770c6d69e68dF8Bd1976459DD8705C26ed8115D100004000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xE770c6d69e68dF8Bd1976459DD8705C26ed8115D100005000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xE770c6d69e68dF8Bd1976459DD8705C26ed8115D100006000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x14cF922aa1512Adfc34409b63e18D391e4a86A2f10000500x00000000000000000000000000000000000000000x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca00xae7ab96520DE3A18E5e111B5EaAb095312D7fE840600xcddE8f9c3414A00f804E5C565Eed9949AD17e8880x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca00xae7ab96520DE3A18E5e111B5EaAb095312D7fE840600x3aC6E14A142251eB3Fe739399e0A8DA81ED068880x00000000000000000000000000000000000000000xBA41Ddf06B7fFD89D1267b5A93BFeF2424eb2003100052000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xBA41Ddf06B7fFD89D1267b5A93BFeF2424eb2003100001000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x157a6df6B74F4E5E45af4E4615FDe7B49225a662100051000x00000000000000000000000000000000000000000x888888888887715FB9D9F84175Af9e6cCE46807e0xb2089A7069861C8D90c8dA3aaCAB8e9188C0C5313000600x65AC0d25cE0Ca8BDDFd7F58f66D1748623698fC00x00000000000000000000000000000000000000000x44108f0223A3C3028F5Fe7AEC7f9bb2E66beF82F100051000x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2010x23CE72f64743a067F2fFA0ba782190bAa4A4e0880x1223334444A7466fBf985B14E1F4EdAf3883bCa60x888888888887715FB9D9F84175Af9e6cCE46807e3000600x3474479b21bc3FB6B24A32f143273E5c3f8d8FC00x19640000000ba88d36206BeB10D0e86011C8d08c0x888888888887715FB9D9F84175Af9e6cCE46807e3000600x08382AB717D6a9F64d0D66e3C15E46c4aC790fc00x888888888887715FB9D9F84175Af9e6cCE46807e0xb10cc888cB2CcE7036F4c7ECAd8a57Da161613383000600x46327A484fE21d039Ba3F886D412B58208e10fC00x600D601D8b9EB5DE5Ac90fEfC68d0d08801bFd3f0x888888888887715FB9D9F84175Af9e6cCE46807e3000600x6Cc5591ec4935C41C472540BdD2174ce881d8fC00x00000000000000000000000000000000000000000x88909D489678dD17aA6D9609F89B0419Bf78FD9a100011000x00000000000000000000000000000000000000000x888888888887715FB9D9F84175Af9e6cCE46807e0xb10CB07CA2CDac77FbB5707f6690301f9d036F453000600x12aDB795cFeB2571d334715F9d087Ce76Fa98fc00x1223334444A7466fBf985B14E1F4EdAf3883bCa60x888888888887715FB9D9F84175Af9e6cCE46807e3000600xB70088471104Ab3aFc825E2D73F66A745e610fC00x00000000000000000000000000000000000000000x5C1d9aA868a30795F92fAe903eDc9eFF269044bf100001000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480327670xB13250f0Dc8ec6dE297E81CDA8142DB51860BaC40x00000000000000000000000000000000000000000x88909D489678dD17aA6D9609F89B0419Bf78FD9a3000500x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x88909D489678dD17aA6D9609F89B0419Bf78FD9a10000500x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828100011000x00000000000000000000000000000000000000000x0B010000b7624eb9B3DfBC279673C76E9D29D5F70xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483000500x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x44108f0223A3C3028F5Fe7AEC7f9bb2E66beF82F100001000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x157a6df6B74F4E5E45af4E4615FDe7B49225a662100001000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828100001000x00000000000000000000000000000000000000000x437cc33344a0B27A429f795ff6B469C72698B2910xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48010x00000000000000000000000000000000000000000x888888888887715FB9D9F84175Af9e6cCE46807e0xf7e945FcE8F19302AaCc7E1418b0A0bdef89327B3000600xE9a1EA39e36175DEaDeADAb2Ed98FF14f9A10fc00xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xacA92E438df0B2401fF60dA7E4337B687a2435DA010xDe400595199E6Dae55a1bcb742B3Eb249AF008000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xacA92E438df0B2401fF60dA7E4337B687a2435DA010x00000000000000000000000000000000000000000x77146784315Ba81904d654466968e3a7c196d1f30xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483000500x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2300010x215D1B184fC2480f3932454D63cB796a3b0220880x00000000000000000000000000000000000000000x4691937a7508860F876c9c0a2a617E7d9E945D4B3000500x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2300010x987e33d3717761C1Ccd2705CF537289367Ea60880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2300010x6bbDcb2d52B1319AED9615035844F56597C8a0880x00000000000000000000000000000000000000000xf94e7d0710709388bCe3161C32B4eEA56d3f91CC250001000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x4691937a7508860F876c9c0a2a617E7d9E945D4B3000400x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xB0fFa8000886e57F86dd5264b9582b2Ad87b2b913001500x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2300010xDeC5d74720c4Cecb42c1BE46854D0977d59F20880x00000000000000000000000000000000000000000x8db036f007841C21B97eFF7dfc2c187241d59BaF100011000x00000000000000000000000000000000000000000x8db036f007841C21B97eFF7dfc2c187241d59BaF0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48100051000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x888888888887715FB9D9F84175Af9e6cCE46807e3000600xF7F07987e702254BF9cBF73036C3a7ad4A608fc00x8db036f007841C21B97eFF7dfc2c187241d59BaF0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48100001000x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xB0fFa8000886e57F86dd5264b9582b2Ad87b2b913000500x00000000000000000000000000000000000000000x553F4cB7256D8fC038E91d36Cb63fa7C13b624AB0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483001500x00000000000000000000000000000000000000000x216ccd651207B500b5cA6cffe802CECFDf37B6280xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc23000600x00000000000000000000000000000000000000000x553F4cB7256D8fC038E91d36Cb63fa7C13b624AB0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB483000500x00000000000000000000000000000000000000000xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc20xDf7Eee53684D0Ce30844024dCc300E0f3E22C1a43000600xDE2cfBd6Df89A81CCb6114114b8923d6239740880x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C5990xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48300010x6bbDcb2d52B1319AED9615035844F56597C8a0880x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C5990xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2300010x6bbDcb2d52B1319AED9615035844F56597C8a0880xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xdAC17F958D2ee523a2206206994597C13D831ec7300010x6bbDcb2d52B1319AED9615035844F56597C8a0880x94314a14Df63779c99C0764a30e0CD22fA78fC0E0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc23000600x00000000000000000000000000000000000000000x94314a14Df63779c99C0764a30e0CD22fA78fC0E0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2500100x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xAcd2c239012D17BEB128B0944D49015104113650105002000x00000000000000000000000000000000000000000x3bA86B4d9e373caB46f29775fDd5a7D4346D46e50xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc23000600x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000x3C7519a40bC88e34DCE2Ecee29AdE241fac72f2e3000600x50A660d4367E074A5780F6c41288283395BCc0440x00000000000000000000000000000000000000000xc9B53AB2679f573e480d01e0f49e2B5CFB7a3EAb3500600x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2300010xE085C44D3c42a733EA74a26Dd8bf7eE2880760880x00000000000000000000000000000000000000000xAcd2c239012D17BEB128B0944D4901510411365010000500x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2300010x66D487F9127569C92C0dC43673079396142AE0880x24A3D725C37A8D1a66Eb87f0E5D07fE67c1200350xdAC17F958D2ee523a2206206994597C13D831ec710001500x00000000000000000000000000000000000000000x24A3D725C37A8D1a66Eb87f0E5D07fE67c1200350xdAC17F958D2ee523a2206206994597C13D831ec730011000x00000000000000000000000000000000000000000x24A3D725C37A8D1a66Eb87f0E5D07fE67c1200350xdAC17F958D2ee523a2206206994597C13D831ec73000500x00000000000000000000000000000000000000000x24A3D725C37A8D1a66Eb87f0E5D07fE67c1200350xdAC17F958D2ee523a2206206994597C13D831ec7100001000x00000000000000000000000000000000000000000x24A3D725C37A8D1a66Eb87f0E5D07fE67c1200350xdAC17F958D2ee523a2206206994597C13D831ec73000400x00000000000000000000000000000000000000000x00000000000000000000000000000000000000000xc9B53AB2679f573e480d01e0f49e2B5CFB7a3EAb3000600x00000000000000000000000000000000000000000x6BEF15D938d4E72056AC92Ea4bDD0D76B1C4ad290xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48105002000x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xd9Fcd98c322942075A5C3860693e9f4f03AAE07b101500x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xD0eC028a3D21533Fdd200838F39c85B03679285D3000500x00000000000000000000000000000000000000000xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB480xd9Fcd98c322942075A5C3860693e9f4f03AAE07b100500x0000000000000000000000000000000000000000 \ No newline at end of file diff --git a/tests/test_asyncuniswap.py b/tests/test_asyncuniswap.py new file mode 100644 index 00000000..529d0011 --- /dev/null +++ b/tests/test_asyncuniswap.py @@ -0,0 +1,650 @@ +import logging +import os +import shutil +import subprocess +from collections.abc import Generator +from contextlib import contextmanager +from dataclasses import dataclass +from time import sleep + +import pytest +import pytest_asyncio +from web3 import AsyncWeb3 +from web3.types import Wei + +from uniswap import AsyncUniswap +from uniswap.constants import ETH_ADDRESS +from uniswap.exceptions import InvalidFeeTier +from uniswap.fee import FeeTier +from uniswap.tokens import get_tokens +from uniswap.util import ( + _addr_to_str, + _str_to_addr, + default_tick_range, +) + +pytestmark = pytest.mark.skipif( + os.getenv("UNISWAP_VERSION") == "4", + reason="This test file is for Uniswap v1, v2, and v3. For Uniswap v4 tests, see test_uniswap4.py", +) + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +ENV_UNISWAP_VERSION = os.getenv("UNISWAP_VERSION", None) +if ENV_UNISWAP_VERSION: + UNISWAP_VERSIONS = [int(ENV_UNISWAP_VERSION)] +else: + UNISWAP_VERSIONS = [1, 2, 3] + +RECEIPT_TIMEOUT = 5 + + +ONE_ETH = 10**18 +ONE_DAI = 10**18 +ONE_USDC = 10**6 + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + +@dataclass +class AnvilInstance: + provider: str + eth_address: str + eth_privkey: str + + +@pytest.fixture(scope="module", params=UNISWAP_VERSIONS) +def client(request, web3: AsyncWeb3, anvil: AnvilInstance): + return AsyncUniswap( + anvil.eth_address, + anvil.eth_privkey, + web3=web3, + version=request.param, + use_estimate_gas=False, # see note in _build_and_send_tx + ) + + +@pytest.fixture(scope="function") +def tokens(client: AsyncUniswap): + return get_tokens(client.netname) + + +@pytest_asyncio.fixture(scope="module") +async def test_assets(client: AsyncUniswap): + """ + Buy some DAI and USDC to test with. + """ + tokens = get_tokens(client.netname) + + for token_name, amount in [ + ("DAI", 10_000 * ONE_DAI), + ("USDC", 10_000 * ONE_USDC), + ]: + token_addr = tokens[token_name] + price = await 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 = await client.make_trade_output( + tokens["ETH"], token_addr, amount, fee=FeeTier.TIER_3000 + ) + tx = await client.w3.eth.wait_for_transaction_receipt( + txid, timeout=RECEIPT_TIMEOUT + ) + assert tx["status"] == 1, f"Transaction failed: {tx}" + + +@pytest_asyncio.fixture(scope="module") +async def web3(anvil: AnvilInstance): + w3: AsyncWeb3 = AsyncWeb3( + AsyncWeb3.AsyncHTTPProvider(anvil.provider, request_kwargs={"timeout": 30}) + ) + if 1 != int(await w3.net.version): + logger.warning("PROVIDER was not a mainnet provider, which the tests require") + return w3 + + +@pytest.fixture(scope="module") +def anvil() -> Generator[AnvilInstance, None, None]: + """Fixture that runs anvil which has forked off mainnet""" + if not shutil.which("anvil"): + raise ValueError( + "anvil was not found in PATH, install Foundry: https://getfoundry.sh" + ) + if "PROVIDER" not in os.environ: + raise ValueError( + "PROVIDER was not set, you need to set it to a mainnet provider (such as Infura) so that we can fork off our testnet" + ) + + port = 11999 + defaultGasPrice = 100_000_000_000 # 100 gwei + p = subprocess.Popen( + f"""anvil + --port {port} + --chain-id 1 + --fork-url {os.environ["PROVIDER"]} + --gas-price {defaultGasPrice} + """.replace("\n", " "), + shell=True, + ) + # Account #9 from anvil's default test mnemonic, starts with 1000 ETH + eth_address = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" + eth_privkey = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" + + sleep(3) + yield AnvilInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) + p.kill() + p.wait() + + +@contextmanager +def does_not_raise(): + yield + + +ONE_ETH = 10**18 +ONE_USDC = 10**6 + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" + + +@pytest.mark.usefixtures("client", "web3") +class TestAsyncUniswap: + # ------ Exchange ------------------------------------------------------------------ + @pytest.mark.asyncio + async def test_get_fee_maker(self, client: AsyncUniswap): + if client.version not in [1, 2]: + pytest.skip("Tested method not supported in this Uniswap version") + r = client.get_fee_maker() + assert r == 0 + + @pytest.mark.asyncio + async def test_get_fee_taker(self, client: AsyncUniswap): + if client.version not in [1, 2]: + pytest.skip("Tested method not supported in this Uniswap version") + r = client.get_fee_taker() + assert r == 0.003 + + # ------ Market -------------------------------------------------------------------- + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0", + [ + ("UNI"), + ("DAI"), + ("USDC"), + ], + ) + async def test_approve(self, client: AsyncUniswap, tokens, token0): + token0 = tokens[token0] + if client.version == 1 and ETH_ADDRESS == token0: + pytest.skip("Not supported in this version of Uniswap") + r = await client.approve(token0) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "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), + ], + ) + async def test_get_price_input( + self, client: AsyncUniswap, 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 = await client.get_price_input(token0, token1, qty, fee=FeeTier.TIER_3000) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "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), + ], + ) + async def test_get_price_output( + self, client: AsyncUniswap, 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 = await client.get_price_output(token0, token1, qty, fee=FeeTier.TIER_3000) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, fee", [("DAI", "USDC", FeeTier.TIER_3000)] + ) + async def test_get_raw_price( + self, client: AsyncUniswap, tokens, token0, token1, fee + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version == 1: + pytest.skip("Only supported on Uniswap v2 and v3") + r = await client.get_raw_price(token0, token1, fee=fee) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, kwargs", + [ + ("WETH", "DAI", {"fee": FeeTier.TIER_3000}), + ], + ) + async def test_get_pool_instance( + self, client: AsyncUniswap, tokens, token0, token1, kwargs + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + r = await client.get_pool_instance(token0, token1, **kwargs) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, kwargs", + [ + ("WETH", "DAI", {"fee": FeeTier.TIER_3000}), + ], + ) + async def test_get_pool_immutables( + self, client: AsyncUniswap, tokens, token0, token1, kwargs + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + pool = await client.get_pool_instance(token0, token1, **kwargs) + r = await client.get_pool_immutables(pool) + print(r) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, kwargs", + [ + ("WETH", "DAI", {"fee": FeeTier.TIER_3000}), + ], + ) + async def test_get_pool_state( + self, client: AsyncUniswap, tokens, token0, token1, kwargs + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + pool = await client.get_pool_instance(token0, token1, **kwargs) + r = await client.get_pool_state(pool) + print(r) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "amount0, amount1, token0, token1, kwargs", + [ + (1, 10, "WETH", "DAI", {"fee": FeeTier.TIER_3000}), + ], + ) + async def test_mint_position( + self, client: AsyncUniswap, tokens, amount0, amount1, token0, token1, kwargs + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + pool = await client.get_pool_instance(token0, token1, **kwargs) + r = await client.mint_position(pool, amount0, amount1) + print(r) + assert r + + # ------ ERC20 Pool ---------------------------------------------------------------- + @pytest.mark.asyncio + @pytest.mark.parametrize("token", [("UNI"), ("DAI")]) + async def test_get_ex_eth_balance( + self, + client: AsyncUniswap, + tokens, + token, + ): + if client.version != 1: + pytest.skip("Only supported on Uniswap v1") + r = await client.get_ex_eth_balance(tokens[token]) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize("token", [("UNI"), ("DAI")]) + async def test_get_ex_token_balance( + self, + client: AsyncUniswap, + tokens, + token, + ): + if client.version != 1: + pytest.skip("Only supported on Uniswap v1") + r = await client.get_ex_token_balance(tokens[token]) + assert r + + @pytest.mark.asyncio + @pytest.mark.parametrize("token", [("UNI"), ("DAI")]) + async def test_get_exchange_rate( + self, + client: AsyncUniswap, + tokens, + token, + ): + if client.version != 1: + pytest.skip("Only supported on Uniswap v1") + r = await client.get_exchange_rate(tokens[token]) + assert r + + # ------ Liquidity ----------------------------------------------------------------- + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token0, token1, amount0, amount1, qty, fee", + [ + ("DAI", "USDC", ONE_ETH, ONE_USDC, ONE_ETH, FeeTier.TIER_3000), + ], + ) + async def test_v3_deploy_pool_with_liquidity( + self, client: AsyncUniswap, tokens, token0, token1, amount0, amount1, qty, fee + ): + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + + try: + pool = await client.create_pool_instance( + tokens[token0], tokens[token1], fee + ) + except Exception: + pool = await client.get_pool_instance(tokens[token0], tokens[token1], fee) + + print(pool.address) + # Ensuring client has sufficient balance of both tokens + eth_to_dai = await client.make_trade( + tokens["ETH"], + tokens[token0], + qty, + client.address, + fee=fee, + ) + eth_to_dai_tx = await client.w3.eth.wait_for_transaction_receipt( + eth_to_dai, timeout=RECEIPT_TIMEOUT + ) + assert eth_to_dai_tx["status"] + dai_to_usdc = await client.make_trade( + tokens[token0], + tokens[token1], + qty * 10, + client.address, + fee=fee, + ) + dai_to_usdc_tx = await client.w3.eth.wait_for_transaction_receipt( + dai_to_usdc, timeout=RECEIPT_TIMEOUT + ) + assert dai_to_usdc_tx["status"] + + balance_0 = await client.get_token_balance(tokens[token0]) + balance_1 = await client.get_token_balance(tokens[token1]) + + assert balance_0 > amount0, f"Have: {balance_0} need {amount0}" + assert balance_1 > amount1, f"Have: {balance_1} need {amount1}" + + min_tick, max_tick = default_tick_range(fee) + r = await client.mint_liquidity( + pool, + amount0, + amount1, + tick_lower=min_tick, + tick_upper=max_tick, + deadline=2**64, + ) + assert r["status"] + + position_balance = await client.nonFungiblePositionManager.functions.balanceOf( + _addr_to_str(client.address) + ).call() + assert position_balance > 0 + + position_array = await client.get_liquidity_positions() + assert len(position_array) > 0 + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "deadline", + [(2**64)], + ) + async def test_close_position(self, client: AsyncUniswap, deadline): + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + position_array = await client.get_liquidity_positions() + tokenId = position_array[0] + r = await client.close_position(tokenId, deadline=deadline) + assert r["status"] + + @pytest.mark.asyncio + @pytest.mark.parametrize("token0, token1", [("DAI", "USDC")]) + async def test_get_tvl_in_pool_on_chain( + self, client: AsyncUniswap, tokens, token0, token1 + ): + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + + pool = await client.get_pool_instance( + tokens[token0], tokens[token1], fee=FeeTier.TIER_3000 + ) + tvl_0, tvl_1 = await client.get_tvl_in_pool(pool) + assert tvl_0 > 0 + assert tvl_1 > 0 + + @pytest.mark.asyncio + @pytest.mark.skip + @pytest.mark.parametrize( + "token, max_eth", + [ + ("UNI", 0.00001 * ONE_ETH), + ("DAI", 0.00001 * ONE_ETH), + ], + ) + async def test_add_liquidity( + self, client: AsyncUniswap, tokens, web3: AsyncWeb3, token, max_eth + ): + token = tokens[token] + r = await client.add_liquidity(token, max_eth) + tx = await web3.eth.wait_for_transaction_receipt(r, timeout=RECEIPT_TIMEOUT) + assert tx["status"] + + @pytest.mark.asyncio + @pytest.mark.skip + @pytest.mark.parametrize( + "token, max_token, expectation", + [ + ("UNI", 0.00001 * ONE_ETH, does_not_raise()), + ("DAI", 0.00001 * ONE_ETH, does_not_raise()), + ], + ) + async def test_remove_liquidity( + self, + client: AsyncUniswap, + web3: AsyncWeb3, + tokens, + token, + max_token, + expectation, + ): + token = tokens[token] + with expectation: + r = await client.remove_liquidity(tokens[token], max_token) + tx = await web3.eth.wait_for_transaction_receipt(r) + assert tx["status"] + + # ------ Make Trade ---------------------------------------------------------------- + @pytest.mark.asyncio + @pytest.mark.parametrize( + "input_token, output_token, qty, recipient, expectation", + [ + # ETH -> Token + ("ETH", "DAI", ONE_ETH, None, does_not_raise), + # Token -> Token + ("DAI", "USDC", ONE_ETH, None, does_not_raise), + # Token -> ETH + ("USDC", "ETH", ONE_USDC // 2, None, does_not_raise), + # ("ETH", "UNI", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), + # ("UNI", "ETH", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), + # ("DAI", "UNI", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), + ], + ) + async def test_make_trade( + self, + client: AsyncUniswap, + web3: AsyncWeb3, + tokens, + test_assets, + input_token, + output_token, + qty: int, + recipient, + expectation, + ): + input_token, output_token = tokens[input_token], tokens[output_token] + if client.version == 1 and ETH_ADDRESS not in [input_token, output_token]: + pytest.skip( + "Not supported in this version of Uniswap, or at least no liquidity" + ) + # Uniswap v1 token-to-ETH uses Vyper 0.1.x bytecode with non-standard JUMP + # patterns that Ganache tolerated but Anvil's strict revm rejects (InvalidJump). + # xfail until v1 is formally deprecated or a compatible fork-mode is found. + if client.version == 1 and output_token == ETH_ADDRESS: + pytest.xfail( + "v1 token-to-ETH: EvmError: InvalidJump — Vyper 0.1.x bytecode " + "incompatible with Anvil revm; tracked for v1 deprecation" + ) + with expectation(): + bal_in_before = await client.get_token_balance(input_token) + + txid = await client.make_trade( + input_token, output_token, qty, recipient, fee=FeeTier.TIER_3000 + ) + tx = await web3.eth.wait_for_transaction_receipt( + txid, timeout=RECEIPT_TIMEOUT + ) + assert tx["status"], f"Transaction failed with status {tx['status']}: {tx}" + + # TODO: Checks for ETH, taking gas into account + bal_in_after = await client.get_token_balance(input_token) + if input_token != tokens["ETH"]: + assert bal_in_before - qty == bal_in_after + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "input_token, output_token, qty, recipient, expectation", + [ + # ETH -> Token + ("ETH", "DAI", ONE_ETH, None, does_not_raise), + # Token -> Token + ("DAI", "USDC", ONE_USDC, None, does_not_raise), + # Token -> ETH + ("DAI", "ETH", ONE_ETH // 10, None, does_not_raise), + # FIXME: These should probably be uncommented eventually + # ("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", "DAI", ONE_USDC, None, lambda: pytest.raises(ValueError)), + ], + ) + async def test_make_trade_output( + self, + client: AsyncUniswap, + web3: AsyncWeb3, + tokens, + test_assets, + input_token, + output_token, + qty: int, + recipient, + expectation, + ): + input_token, output_token = tokens[input_token], tokens[output_token] + if client.version == 1 and ETH_ADDRESS not in [input_token, output_token]: + pytest.skip( + "Not supported in this version of Uniswap, or at least no liquidity" + ) + # Same Anvil revm InvalidJump for v1 token-to-ETH (see test_make_trade above). + if client.version == 1 and output_token == ETH_ADDRESS: + pytest.xfail( + "v1 token-to-ETH: EvmError: InvalidJump — Vyper 0.1.x bytecode " + "incompatible with Anvil revm; tracked for v1 deprecation" + ) + with expectation(): + balance_before = await client.get_token_balance(output_token) + + r = await client.make_trade_output( + input_token, output_token, qty, recipient, fee=FeeTier.TIER_3000 + ) + tx = await web3.eth.wait_for_transaction_receipt(r, timeout=RECEIPT_TIMEOUT) + assert tx["status"] + + # # TODO: Checks for ETH, taking gas into account + balance_after = await client.get_token_balance(output_token) + if output_token != tokens["ETH"]: + assert balance_before + qty == balance_after + + @pytest.mark.asyncio + async def test_fee_required_for_uniswap_v3( + self, + client: AsyncUniswap, + tokens, + ) -> None: + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + with pytest.raises(InvalidFeeTier): + await client.get_price_input( + tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None + ) + with pytest.raises(InvalidFeeTier): + await client.get_price_output( + tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None + ) + with pytest.raises(InvalidFeeTier): + await client._get_eth_token_output_price(tokens["UNI"], ONE_ETH, fee=None) + with pytest.raises(InvalidFeeTier): + await client._get_token_eth_output_price( + tokens["UNI"], Wei(ONE_ETH), fee=None + ) + with pytest.raises(InvalidFeeTier): + await client._get_token_token_output_price( + tokens["UNI"], tokens["ETH"], ONE_ETH, fee=None + ) + with pytest.raises(InvalidFeeTier): + await client.make_trade(tokens["ETH"], tokens["UNI"], ONE_ETH, fee=None) + with pytest.raises(InvalidFeeTier): + await 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): + await client.get_pool_instance(tokens["ETH"], tokens["UNI"], fee=None) # type: ignore[arg-type] + with pytest.raises(InvalidFeeTier): + await client.create_pool_instance(tokens["ETH"], tokens["UNI"], fee=None) # type: ignore[arg-type] + with pytest.raises(InvalidFeeTier): + await client.get_raw_price(tokens["ETH"], tokens["UNI"], fee=None) diff --git a/tests/test_asyncuniswap4.py b/tests/test_asyncuniswap4.py new file mode 100644 index 00000000..59da6b64 --- /dev/null +++ b/tests/test_asyncuniswap4.py @@ -0,0 +1,1885 @@ +import logging +import os +import shutil +import subprocess +from collections.abc import Generator +from contextlib import contextmanager +from dataclasses import astuple, dataclass +from time import sleep + +import pytest +import pytest_asyncio +from web3 import AsyncWeb3 +from web3.types import Nonce + +from uniswap import AsyncUniswap4 +from uniswap.constants import ( + ETH_ADDRESS, + ZERO_HOOK, + universal_router_commands, + v4_actions, +) +from uniswap.types import AddressLike, PoolKey +from uniswap.util import AsyncV4pools, V4pools, _addr_to_str, _str_to_addr + +pytestmark = pytest.mark.skipif( + os.getenv("UNISWAP_VERSION") != "4", + reason="This test file is for Uniswap v4. For Uniswap v1, v2, and v3 tests, see test_uniswap.py", +) + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +RECEIPT_TIMEOUT = 5 + +ONE_ETH = 10**18 +ONE_USDT = 10**6 +ONE_USDC = 10**6 +ETH_DECIMALS = 18 +USDT_DECIMALS = 6 +USDC_DECIMALS = 6 +USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" +USDT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7" +ETH_USDC_FEE = 500 +ETH_USDC_TICK_SPACING = 10 +USDC_USDT_FEE = 10 +USDC_USDT_TICK_SPACING = 1 + +TOKEN_ID = 1 + +eth_usdc_poolkey: PoolKey = PoolKey( + currency0=ETH_ADDRESS, # ETH + currency1=USDC_ADDRESS, # USDC + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + hooks=ZERO_HOOK, +) + + +usdc_usdt_poolkey: PoolKey = PoolKey( + currency0=USDC_ADDRESS, # USDC + currency1=USDT_ADDRESS, # USDT + fee=USDC_USDT_FEE, + tick_spacing=USDC_USDT_TICK_SPACING, + hooks=ZERO_HOOK, +) + + +@dataclass +class AnvilInstance: + provider: str + eth_address: str + eth_privkey: str + + +@pytest.fixture(scope="module") +def client(web3: AsyncWeb3, anvil: AnvilInstance) -> AsyncUniswap4: + return AsyncUniswap4( + anvil.eth_address, + anvil.eth_privkey, + web3=web3, + gas_limit=500_000, + ) + + +@pytest.fixture(scope="module") +def pool_service(web3: AsyncWeb3) -> AsyncV4pools: + return AsyncV4pools(web3) + + +@pytest_asyncio.fixture(scope="module") +async def web3(anvil: AnvilInstance) -> AsyncWeb3: + w3 = AsyncWeb3( + AsyncWeb3.AsyncHTTPProvider(anvil.provider, request_kwargs={"timeout": 30}) + ) + if 1 != int(await w3.net.version): + logger.warning("PROVIDER was not a mainnet provider, which the tests require") + return w3 + + +@pytest.fixture(scope="module") +def anvil() -> Generator[AnvilInstance, None, None]: + """Fixture that runs anvil which has forked off mainnet""" + if not shutil.which("anvil"): + raise ValueError("anvil was not found in PATH") + if "PROVIDER" not in os.environ: + raise ValueError( + "PROVIDER was not set, you need to set it to a mainnet provider (such as Infura) so that we can fork off our testnet" + ) + + port = 11998 + defaultGasPrice = 100_000_000_000 # 100 gwei + p = subprocess.Popen( + f"""anvil + --port {port} + --chain-id 1 + --fork-url {os.environ["PROVIDER"]} + --gas-price {defaultGasPrice} + """.replace("\n", " "), + shell=True, + ) + # Address #1 when anvil is run with `--wallet.seed test`, it starts with 1000 ETH + eth_address = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" + eth_privkey = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" + sleep(3) + yield AnvilInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) + p.kill() + p.wait() + + +@contextmanager +def does_not_raise(): + yield + + +@pytest.mark.usefixtures("client", "web3") +class TestAsyncUniswap4: + test_token_id: int = 0 + test_mint_tx_hash: str = "" + test_tick: int = 0 + + # ------ Approve/tx replacement----------------------------------------------------- + @pytest.mark.asyncio + @pytest.mark.parametrize( + "token, max_approval, delay_interval", + [ + (USDC_ADDRESS, None, 7), + (USDT_ADDRESS, 1_000_000 * ONE_USDT, 1), + ], + ) + async def test_approve( + self, + client: AsyncUniswap4, + token: str, + max_approval: int | None, + delay_interval: int, + ): + # Approve the token + tx = await client.approve( + _str_to_addr(token), + max_approval, + delay_interval=delay_interval, + approve_position_manager=True, + ) + assert tx + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + # Check that the approval was successful by calling allowance + allowance = await client.approval(_str_to_addr(token)) + assert allowance + + @pytest.mark.asyncio + @pytest.mark.parametrize( + "address_to, gas_price, priority_fee, custom_nonce", + [ + ("self", 20, 10, None), + (ETH_ADDRESS, 20, 10, 0), + ], + ) + async def test_drop_txn( + self, + client: AsyncUniswap4, + address_to: str, + gas_price: float, + priority_fee: int, + custom_nonce: int | None, + ): + if not client.w3.is_address(address_to): + address: AddressLike = client.address + else: + address = _str_to_addr(address_to) + await client.update_last_nonce() + if custom_nonce == 0: + nonce: Nonce | None = client.last_nonce + else: + nonce = None + + tx = await client.drop_txn( + address, + gas_price, + priority_fee=priority_fee, + custom_nonce=nonce, + ) + assert tx + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + await client.update_last_nonce() + + # ------ Market/price impact-------------------------------------------------------- + # Input quotes + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_get_quote_exact_input_single( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.get_quote_exact_input_single( + token0, token1, qty, fee, tick_spacing, hooks + ) + assert result + + @pytest.mark.parametrize( + "token0, qty, route", + [ + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + ], + ), + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + ), + ], + ) + @pytest.mark.asyncio + async def test_get_quote_exact_input( + self, + client: AsyncUniswap4, + token0: str, + qty: int, + route: list[PoolKey], + ): + result = await client.get_quote_exact_input(token0, qty, route) + assert result + + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, route", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + ONE_ETH, + None, + None, + None, + None, + [eth_usdc_poolkey, usdc_usdt_poolkey], + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDT, + None, + None, + None, + None, + [usdc_usdt_poolkey, eth_usdc_poolkey], + ), + ], + ) + @pytest.mark.asyncio + async def test_get_price_input( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int | None, + tick_spacing: int | None, + hooks: str | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + ): + result = await client.get_price_input( + token0, token1, qty, fee, tick_spacing, hooks, hook_data, route + ) + assert result + + # Output quotes + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_get_quote_exact_output_single( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.get_quote_exact_output_single( + token0, token1, qty, fee, tick_spacing, hooks + ) + assert result + + @pytest.mark.parametrize( + "token0, qty, route", + [ + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + ], + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + ), + ], + ) + @pytest.mark.asyncio + async def test_get_quote_exact_output( + self, client: AsyncUniswap4, token0: str, qty: int, route: list[PoolKey] + ): + result = await client.get_quote_exact_output(token0, qty, route) + assert result + + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, route", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + 1000 * ONE_USDT, + None, + None, + None, + None, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + None, + None, + None, + None, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + ), + ], + ) + @pytest.mark.asyncio + async def test_get_price_output( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int | None, + tick_spacing: int | None, + hooks: str | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + ): + result = await client.get_price_output( + token0, token1, qty, fee, tick_spacing, hooks, hook_data, route + ) + assert result + + # Price impact/spot price + @pytest.mark.parametrize( + "token0, token1, test_volume", + [ + (ETH_ADDRESS, USDC_ADDRESS, ONE_ETH), + (ETH_ADDRESS, USDC_ADDRESS, 10 * ONE_ETH), + (ETH_ADDRESS, USDC_ADDRESS, 100 * ONE_ETH), + (ETH_ADDRESS, USDC_ADDRESS, 1000 * ONE_ETH), + ], + ) + @pytest.mark.asyncio + async def test_estimate_price_impact( + self, client: AsyncUniswap4, token0: str, token1: str, test_volume: int + ): + result = await client.estimate_price_impact(token0, token1, test_volume) + assert result + + @pytest.mark.parametrize( + "token0, token1", + [ + (ETH_ADDRESS, USDC_ADDRESS), + (USDC_ADDRESS, ETH_ADDRESS), + ], + ) + @pytest.mark.asyncio + async def test_get_token_token_spot_price( + self, client: AsyncUniswap4, token0: str, token1: str + ): + result = await client.get_token_token_spot_price(token0, token1) + assert result + + # ------ Swaps---------------------------------------------------------------------- + # Input swaps + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_token_to_token_swap_exact_input( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + qtycap = await client.get_quote_exact_input_single( + token0, token1, qty, fee, tick_spacing, hooks, hook_data + ) + tx = await client.token_to_token_swap_exact_input( + token0, + qty, + qtycap, + token1, + fee, + tick_spacing, + hooks, + hook_data, + 0, # min_hop_price_x_36 + custom_nonce, + ) + assert tx + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, qty, route, custom_nonce", + [ + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + ], + None, + ), + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + None, + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_token_to_token_swap_input( + self, + client: AsyncUniswap4, + token0: str, + qty: int, + route: list[PoolKey], + custom_nonce: Nonce | None, + ): + qtycap = await client.get_quote_exact_input(token0, qty, route) + + tx = await client.token_to_token_swap_input( + token0, qty, qtycap, route, min_hop_price_x_36=[], custom_nonce=custom_nonce + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, token1, qty, pool_key, hook_data, route, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 10 * ONE_ETH, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDC, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + 10 * ONE_ETH, + None, + None, + [eth_usdc_poolkey, usdc_usdt_poolkey], + None, + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDT, + None, + None, + [usdc_usdt_poolkey, eth_usdc_poolkey], + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_make_swap_input( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + pool_key: PoolKey | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + custom_nonce: Nonce | None, + ): + qtycap = await client.get_price_input( + token0, + token1, + qty, + None if pool_key is None else pool_key.fee, + None if pool_key is None else pool_key.tick_spacing, + None if pool_key is None else pool_key.hooks, + hook_data, + route, + ) + tx = await client.make_swap_input( + token0, token1, qty, qtycap, pool_key, hook_data, route, custom_nonce + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # Output swaps + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_token_to_token_swap_exact_output( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + qtycap = await client.get_quote_exact_output_single( + token0, token1, qty, fee, tick_spacing, hooks, hook_data + ) + tx = await client.token_to_token_swap_exact_output( + token0, + qty, + qtycap, + token1, + fee, + tick_spacing, + hooks, + hook_data, + 0, # min_hop_price_x_36 + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, qty, route, custom_nonce", + [ + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + None, + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_token_to_token_swap_output( + self, + client: AsyncUniswap4, + token0: str, + qty: int, + route: list[PoolKey], + custom_nonce: Nonce | None, + ): + qtycap = await client.get_quote_exact_output(token0, qty, route) + tx = await client.token_to_token_swap_output( + token0, qty, qtycap, route, min_hop_price_x_36=[], custom_nonce=custom_nonce + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, token1, qty, pool_key, hook_data, route, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + 1000 * ONE_USDT, + None, + None, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + None, + None, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_make_swap_output( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + qty: int, + pool_key: PoolKey | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + custom_nonce: Nonce | None, + ): + qtycap = await client.get_price_output( + token0, + token1, + qty, + None if pool_key is None else pool_key.fee, + None if pool_key is None else pool_key.tick_spacing, + None if pool_key is None else pool_key.hooks, + hook_data, + route, + ) + tx = await client.make_swap_output( + token0, token1, qty, qtycap, pool_key, hook_data, route, custom_nonce + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # ------ Liquidity -------------------------------------------------------------------- + + @pytest.mark.parametrize( + "pool_key, custom_nonce", + [ + (eth_usdc_poolkey, None), + ], + ) + @pytest.mark.asyncio + async def test_create_pool( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + custom_nonce: Nonce | None, + ): + sqrt_price_x96 = 1 << 96 # 1:1 price + test_pool_key = PoolKey( + currency0=pool_key.currency0, + currency1=pool_key.currency1, + fee=pool_key.fee + 100, + tick_spacing=pool_key.tick_spacing + 500, + hooks=pool_key.hooks, + ) + tx = await client.create_pool(test_pool_key, sqrt_price_x96, custom_nonce) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "pool_key, tick_lower, tick_upper, liquidity, amount0, amount1, hook_data, custom_nonce", + [ + ( + eth_usdc_poolkey, + -600, + 600, + ONE_ETH, + ONE_ETH, + 2500 * ONE_USDC, + b"", + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_mint_position( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + tick_lower: int, + tick_upper: int, + liquidity: int, + amount0: int, + amount1: int, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = await client.mint_position( + pool_key, + tick_lower, + tick_upper, + liquidity, + amount0, + amount1, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + test_token_id_result = await client.get_minted_token_id( + tx_receipt["transactionHash"].hex() + ) + assert len(test_token_id_result) > 0 + TestAsyncUniswap4.test_token_id = test_token_id_result[0] + TestAsyncUniswap4.test_mint_tx_hash = tx_receipt["transactionHash"].hex() + + @pytest.mark.asyncio + async def test_get_minted_token_id( + self, + client: AsyncUniswap4, + ): + result = await client.get_minted_token_id(TestAsyncUniswap4.test_mint_tx_hash) + assert result + + @pytest.mark.asyncio + async def test_get_position_info( + self, + client: AsyncUniswap4, + ): + result = await client.get_position_info(TestAsyncUniswap4.test_token_id) + test_pool_id_result: int = int.from_bytes(result["poolID"], byteorder="big") + truncated_pool_id_str = f"{test_pool_id_result:x}".lower() + pool_id_str = ( + client.get_pool_id( + PoolKey( + result["currency0"], + result["currency1"], + result["fee"], + result["tickSpacing"], + result["hooks"], + ) + ) + .hex() + .lower() + ) + assert truncated_pool_id_str == pool_id_str[: len(truncated_pool_id_str)] + + @pytest.mark.parametrize( + "token0_decimals, token1_decimals", + [(ETH_DECIMALS, USDC_DECIMALS)], + ) + @pytest.mark.asyncio + async def test_get_position_value( + self, + client: AsyncUniswap4, + token0_decimals: int, + token1_decimals: int, + ): + result = await client.get_position_value( + TestAsyncUniswap4.test_token_id, token0_decimals, token1_decimals + ) + assert result + + @pytest.mark.parametrize( + "pool_key, liquidity, amount0, amount1, hook_data, custom_nonce", + [ + ( + eth_usdc_poolkey, + int(0.05 * ONE_ETH), + int(0.5 * ONE_ETH), + 1250 * ONE_USDC, + b"", + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_increase_liquidity( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + liquidity: int, + amount0: int, + amount1: int, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = await client.increase_liquidity( + pool_key, + TestAsyncUniswap4.test_token_id, + amount0, + amount1, + liquidity, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "pool_key, hook_data, custom_nonce", + [ + (eth_usdc_poolkey, b"", None), + ], + ) + @pytest.mark.asyncio + async def test_collect_fees( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = await client.collect_fees( + pool_key, + TestAsyncUniswap4.test_token_id, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "pool_key, liquidity, amount0, amount1, hook_data, custom_nonce", + [ + ( + eth_usdc_poolkey, + int(0.05 * ONE_ETH), + 0, + 0, + b"", + None, + ), + ], + ) + @pytest.mark.asyncio + async def test_decrease_liquidity( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + liquidity: int, + amount0: int, + amount1: int, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = await client.decrease_liquidity( + pool_key, + TestAsyncUniswap4.test_token_id, + amount0, + amount1, + liquidity, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # ------ V4Pools tests ---------------------------------------------------------------- + # @pytest.mark.skip( + # reason="Test node issues with fetching poolkey data, skipping for now" + # ) + @pytest.mark.asyncio + @pytest.mark.parametrize( + "first_block", + [ + (21688329), + ], + ) + async def test_fetch_poolkey_data( + self, + pool_service: AsyncV4pools, + first_block: int, + ): + result: int = await pool_service.fetch_poolkey_data( + first_block, + chunk_size=5, + clear_list=False, + retry_attempts=1, + minutes_between_retries=1, + last_block=first_block + 101, + ) + if result != 0: + pytest.skip( + f"Failed to fetch poolkey data, connection issues, result: {result}" + ) + assert result == 0 + + @pytest.mark.parametrize( + "test_data_file_path", + [ + "pool_list.test", + ], + ) + def test_load_poolkeys_list( + self, + pool_service: V4pools, + test_data_file_path: str, + ): + test_data_file_full_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), test_data_file_path + ) + pool_service.load_poolkeys_list(test_data_file_full_path) + assert len(pool_service.poolkeys_list) > 0 + + @pytest.mark.parametrize( + "test_data_file_path", + [ + "pool_list_dump.test", + ], + ) + def test_save_poolkeys_list( + self, + pool_service: V4pools, + test_data_file_path: str, + tmp_path, + ): + test_data_file_full_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "pool_list.test" + ) + pool_service.load_poolkeys_list(test_data_file_full_path) + temp_dir = tmp_path / "temp" + temp_dir.mkdir() + test_data_dump_full_path = temp_dir / test_data_file_path + pool_service.save_poolkeys_list(test_data_dump_full_path) + assert len(test_data_dump_full_path.read_text()) > 0 + + @pytest.mark.parametrize( + "currecy0, currency1", + [ + (ETH_ADDRESS, USDC_ADDRESS), + (USDC_ADDRESS, USDT_ADDRESS), + ], + ) + def test_get_poolkeys_sublist( + self, + pool_service: V4pools, + currecy0: str, + currency1: str, + ): + test_data_file_full_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "pool_list.test" + ) + pool_service.load_poolkeys_list(test_data_file_full_path) + + result = pool_service.get_poolkeys_sublist(currecy0, currency1) + assert len(result) > 0 + + # ------ StateView tests -------------------------------------------------------------- + # Read methods + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_liquidity( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_liquidity( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_slot0( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_slot0( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + assert result + TestAsyncUniswap4.test_tick = int(result["tick"]) + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_fee_growth_globals( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_fee_growth_globals( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_fee_growth_inside( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_fee_growth_inside( + token0, + token1, + fee, + tick_spacing, + hooks, + TestAsyncUniswap4.test_tick - tick_spacing * 50, + TestAsyncUniswap4.test_tick + tick_spacing * 50, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_position_info( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_position_info( + token0, + token1, + fee, + tick_spacing, + hooks, + _addr_to_str(client.address), + -600, + 600, + TestAsyncUniswap4.test_token_id, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_tick_bitmap( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_tick_bitmap( + token0, + token1, + fee, + tick_spacing, + hooks, + -30000, + ) + assert isinstance(result, int) + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_tick_fee_growth_outside( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_tick_fee_growth_outside( + token0, + token1, + fee, + tick_spacing, + hooks, + TestAsyncUniswap4.test_tick, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + @pytest.mark.asyncio + async def test_stateview_get_tick_pool_info( + self, + client: AsyncUniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = await client.stateview_get_tick_pool_info( + token0, + token1, + fee, + tick_spacing, + hooks, + TestAsyncUniswap4.test_tick, + ) + assert result + + # ------ PositionDescriptor tests ----------------------------------------------------- + # Read methods + @pytest.mark.parametrize( + "token0", + [ + (USDC_ADDRESS), + (USDT_ADDRESS), + ], + ) + @pytest.mark.asyncio + async def test_position_descriptor_get_currency_ratio_priority( + self, client: AsyncUniswap4, token0: str + ): + result = await client.position_descriptor_get_currency_ratio_priority(token0) + assert isinstance(result, int) + + @pytest.mark.parametrize( + "token0, token1", + [ + (USDC_ADDRESS, ETH_ADDRESS), + (USDT_ADDRESS, ETH_ADDRESS), + ], + ) + @pytest.mark.asyncio + async def test_position_descriptor_get_flip_ratio( + self, client: AsyncUniswap4, token0: str, token1: str + ): + result = await client.position_descriptor_get_flip_ratio(token0, token1) + assert isinstance(result, bool) + + @pytest.mark.asyncio + async def test_position_descriptor_get_native_currency_label( + self, client: AsyncUniswap4 + ): + result = await client.position_descriptor_get_native_currency_label() + assert result + + @pytest.mark.asyncio + async def test_position_descriptor_get_pool_manager(self, client: AsyncUniswap4): + result = await client.position_descriptor_get_pool_manager() + assert result + + @pytest.mark.asyncio + async def test_position_descriptor_get_token_uri(self, client: AsyncUniswap4): + result = await client.position_descriptor_get_token_uri( + _addr_to_str(client.position_manager_address), + TestAsyncUniswap4.test_token_id, + ) + assert result + + @pytest.mark.asyncio + async def test_position_descriptor_get_wrapped_native_address( + self, client: AsyncUniswap4 + ): + result = await client.position_descriptor_get_wrapped_native_address() + assert result + + # ------ PositionManager tests -------------------------------------------------------- + # Read methods + + # Write methods + + # ------ PoolManager tests ------------------------------------------------------------ + # Read methods + + # Write methods + + # Burn test position. + @pytest.mark.parametrize( + "pool_key, hook_data, custom_nonce", + [ + (eth_usdc_poolkey, b"", None), + ], + ) + @pytest.mark.asyncio + async def test_burn_position( + self, + client: AsyncUniswap4, + pool_key: PoolKey, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + # Removing liquidity before burning position, otherwise burn will revert since position is not empty + tx = await client.decrease_liquidity( + pool_key, + TestAsyncUniswap4.test_token_id, + 0, + 0, + ( + await client.stateview_get_position_info( + pool_key.currency0, + pool_key.currency1, + pool_key.fee, + pool_key.tick_spacing, + pool_key.hooks, + recipient, + -600, + 600, + TestAsyncUniswap4.test_token_id, + ) + )["liquidity"], + recipient, + hook_data, + custom_nonce, + ) + assert tx + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # Now burn the position + tx = await client.burn_position( + pool_key, + TestAsyncUniswap4.test_token_id, + 0, + 0, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # ------ UniversalRouter tests -------------------------------------------------------- + @pytest.mark.asyncio + async def test_universal_router_execute_simple_command( + self, + client: AsyncUniswap4, + ): + commands: list = [ + universal_router_commands["WRAP_ETH"], + ] + actions: list = [ + [], + ] + params: list = [ + [ + [ + _addr_to_str(client.address), + 1 * ONE_ETH, + ], + ], + ] + tx = await client.universal_router_execute( + commands, actions, params, ether_amount=1 * ONE_ETH + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.asyncio + async def test_universal_router_execute_composite_command( + self, + client: AsyncUniswap4, + ): + if eth_usdc_poolkey.currency0.lower() < eth_usdc_poolkey.currency1.lower(): + zero_for_one: bool = True + else: + zero_for_one = False + qty: int = 1 * ONE_ETH + qtycap: int = await client.get_quote_exact_input_single( + eth_usdc_poolkey.currency0, + eth_usdc_poolkey.currency1, + qty, + eth_usdc_poolkey.fee, + eth_usdc_poolkey.tick_spacing, + eth_usdc_poolkey.hooks, + b"", + ) + qtycap = int((1 - client.max_slippage) * qtycap) + + commands: list = [ + universal_router_commands["V4_SWAP"], + ] + actions: list = [ + [ + v4_actions["SWAP_EXACT_IN_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ] + + params: list = [ + [ + [ + ( + astuple(eth_usdc_poolkey), + zero_for_one, + qty, + qtycap, + 0, # min_hop_price_x_36 + b"", + ) + ], + [ + eth_usdc_poolkey.currency0, + qty, + ], + [ + eth_usdc_poolkey.currency1, + qtycap, + ], + ], + ] + tx = await client.universal_router_execute( + commands, actions, params, ether_amount=qty + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.asyncio + async def test_universal_router_execute_multiaction( + self, + client: AsyncUniswap4, + ): + if eth_usdc_poolkey.currency0.lower() < eth_usdc_poolkey.currency1.lower(): + zero_for_one: bool = True + else: + zero_for_one = False + qty: int = 1 * ONE_ETH + qtycap: int = await client.get_quote_exact_input_single( + eth_usdc_poolkey.currency0, + eth_usdc_poolkey.currency1, + qty, + eth_usdc_poolkey.fee, + eth_usdc_poolkey.tick_spacing, + eth_usdc_poolkey.hooks, + b"", + ) + qtycap = int((1 - client.max_slippage) * qtycap) + ether_value = ONE_ETH + + # Executes a WRAP_ETH followed by a V4_SWAP in the same transaction + commands: list = [ + universal_router_commands["WRAP_ETH"], + universal_router_commands["V4_SWAP"], + ] + + # List of actions for each command, in this case we have 0 action for the WRAP_ETH command and 3 actions for the V4_SWAP command (swap, settle and take) + # As WRAP_ETH command does not require any action, we pass an empty list for it + actions: list = [ + [], + [ + v4_actions["SWAP_EXACT_IN_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ] + + # List of parameters for each action, the first element of the list corresponds to the parameters for the WRAP_ETH command (recipient and amount), + # and the second element corresponds to the parameters for each of the 3 actions of the V4_SWAP command + params: list = [ + [ + [ + _addr_to_str(client.address), + ether_value, + ], + ], + [ + [ + ( + astuple(eth_usdc_poolkey), + zero_for_one, + qty, + qtycap, + 0, # min_hop_price_x_36 + b"", + ) + ], + [ + eth_usdc_poolkey.currency0, + qty, + ], + [ + eth_usdc_poolkey.currency1, + qtycap, + ], + ], + ] + # Both commands use ether as input token, so we set the ether_amount to the sum of the ether required for each commands + tx = await client.universal_router_execute( + commands, actions, params, ether_amount=(ether_value + qty) + ) + assert tx + + tx_receipt = await client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..5607c1f1 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,56 @@ +import os +import sys + +import pytest +from click.testing import CliRunner + +from uniswap.cli import main + +pytestmark = pytest.mark.skipif( + os.getenv("UNISWAP_VERSION") == "4", + reason="Not supported in v4", +) + + +def print_result(result): + print(result) + print(result.stdout.strip()) + print(result.stderr.strip(), file=sys.stderr) + + +def test_get_price(): + runner = CliRunner(mix_stderr=False) + result = runner.invoke(main, ["price", "eth", "dai"]) + print_result(result) + assert result.exit_code == 0 + + # Will break when ETH breaks 10k + assert 1000 < float(result.stdout) < 10_000 + + +def test_get_price_stables(): + """Tests that decimals are handled correctly.""" + if os.getenv("UNISWAP_VERSION") == "1": + pytest.skip("Not supported in v1") + + runner = CliRunner(mix_stderr=False) + result = runner.invoke(main, ["price", "dai", "usdc"]) + print_result(result) + assert result.exit_code == 0 + + # Will break if peg is lost + assert 0.9 < float(result.stdout) < 1.1 + + +def test_get_token(): + runner = CliRunner(mix_stderr=False) + result = runner.invoke(main, ["token", "weth"]) + print_result(result) + assert result.exit_code == 0 + + +def test_get_tokendb(): + runner = CliRunner(mix_stderr=False) + result = runner.invoke(main, ["tokendb", "--metadata"]) + print_result(result) + assert result.exit_code == 0 diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index ccc0c2df..bd2d2e85 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -1,60 +1,136 @@ -import pytest +import logging import os -import subprocess import shutil -from typing import Generator +import subprocess +from collections.abc import Generator from contextlib import contextmanager from dataclasses import dataclass from time import sleep +import pytest from web3 import Web3 +from web3.types import Wei + +from uniswap import Uniswap +from uniswap.constants import ETH_ADDRESS +from uniswap.exceptions import InvalidFeeTier +from uniswap.fee import FeeTier +from uniswap.tokens import get_tokens +from uniswap.util import ( + _addr_to_str, + _str_to_addr, + default_tick_range, +) + +pytestmark = pytest.mark.skipif( + os.getenv("UNISWAP_VERSION") == "4", + reason="This test file is for Uniswap v1, v2, and v3. For Uniswap v4 tests, see test_uniswap4.py", +) + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +ENV_UNISWAP_VERSION = os.getenv("UNISWAP_VERSION", None) +if ENV_UNISWAP_VERSION: + UNISWAP_VERSIONS = [int(ENV_UNISWAP_VERSION)] +else: + UNISWAP_VERSIONS = [1, 2, 3] + +RECEIPT_TIMEOUT = 5 -from uniswap import Uniswap, InvalidToken, InsufficientBalance + +ONE_ETH = 10**18 +ONE_DAI = 10**18 +ONE_USDC = 10**6 + +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" @dataclass -class GanacheInstance: +class AnvilInstance: provider: str eth_address: str eth_privkey: str -@pytest.fixture(scope="module", params=[1, 2]) -def client(request, web3: Web3, ganache: GanacheInstance): - uniswap = Uniswap( - ganache.eth_address, ganache.eth_privkey, web3=web3, version=request.param +@pytest.fixture(scope="module", params=UNISWAP_VERSIONS) +def client(request, web3: Web3, anvil: AnvilInstance): + return Uniswap( + anvil.eth_address, + anvil.eth_privkey, + web3=web3, + version=request.param, + use_estimate_gas=False, # see note in _build_and_send_tx ) - uniswap._buy_test_assets() - return uniswap + + +@pytest.fixture(scope="function") +def tokens(client: Uniswap): + return get_tokens(client.netname) @pytest.fixture(scope="module") -def web3(ganache: GanacheInstance): - return Web3(Web3.HTTPProvider(ganache.provider, request_kwargs={"timeout": 60})) +def test_assets(client: Uniswap): + """ + Buy some DAI and USDC to test with. + """ + tokens = get_tokens(client.netname) + + for token_name, amount in [ + ("DAI", 10_000 * ONE_DAI), + ("USDC", 10_000 * ONE_USDC), + ]: + token_addr = tokens[token_name] + 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, fee=FeeTier.TIER_3000 + ) + tx = client.w3.eth.wait_for_transaction_receipt(txid, timeout=RECEIPT_TIMEOUT) + assert tx["status"] == 1, f"Transaction failed: {tx}" @pytest.fixture(scope="module") -def ganache() -> Generator[GanacheInstance, None, None]: - """Fixture that runs ganache-cli which has forked off mainnet""" - if not shutil.which("ganache-cli"): - raise Exception( - "ganache-cli was not found in PATH, you can install it with `npm install -g ganache-cli`" +def web3(anvil: AnvilInstance): + w3 = Web3(Web3.HTTPProvider(anvil.provider, request_kwargs={"timeout": 30})) + if 1 != int(w3.net.version): + logger.warning("PROVIDER was not a mainnet provider, which the tests require") + return w3 + + +@pytest.fixture(scope="module") +def anvil() -> Generator[AnvilInstance, None, None]: + """Fixture that runs anvil which has forked off mainnet""" + if not shutil.which("anvil"): + raise ValueError( + "anvil was not found in PATH, install Foundry: https://getfoundry.sh" ) - if "MAINNET_PROVIDER" not in os.environ: - raise Exception( - "MAINNET_PROVIDER was not set, you need to set it to a mainnet provider (such as Infura) so that we can fork off our testnet" + if "PROVIDER" not in os.environ: + raise ValueError( + "PROVIDER was not set, you need to set it to a mainnet provider (such as Infura) so that we can fork off our testnet" ) port = 10999 + defaultGasPrice = 100_000_000_000 # 100 gwei p = subprocess.Popen( - f"ganache-cli --port {port} -s test --networkId 1 --fork {os.environ['MAINNET_PROVIDER']}", + f"""anvil + --port {port} + --chain-id 1 + --fork-url {os.environ["PROVIDER"]} + --gas-price {defaultGasPrice} + """.replace("\n", " "), shell=True, ) - # Address #1 when ganache is run with `-s test`, it starts with 100 ETH - eth_address = "0x94e3361495bD110114ac0b6e35Ed75E77E6a6cFA" - eth_privkey = "0x6f1313062db38875fb01ee52682cbf6a8420e92bfbc578c5d4fdc0a32c50266f" + # Account #9 from anvil's default test mnemonic, starts with 1000 ETH + eth_address = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" + eth_privkey = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" + sleep(3) - yield GanacheInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) + yield AnvilInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) p.kill() p.wait() @@ -64,257 +140,455 @@ def does_not_raise(): yield -# TODO: Change pytest.param(..., mark=pytest.mark.xfail) to the expectation/raises method -@pytest.mark.usefixtures("client", "web3") -class TestUniswap(object): - ONE_WEI = 1 - ONE_ETH = 10 ** 18 * ONE_WEI - - ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" +ONE_ETH = 10**18 +ONE_USDC = 10**6 - # TODO: Detect mainnet vs rinkeby and set accordingly, like _get_token_addresses in the Uniswap class - # For Mainnet testing (with `ganache-cli --fork` as per the ganache fixture) - eth = "0x0000000000000000000000000000000000000000" - weth = Web3.toChecksumAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") - bat = Web3.toChecksumAddress("0x0D8775F648430679A709E98d2b0Cb6250d2887EF") - dai = Web3.toChecksumAddress("0x6b175474e89094c44da98b954eedeac495271d0f") +ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" - # For Rinkeby - # eth = "0x0000000000000000000000000000000000000000" - # bat = "0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B" - # dai = "0x2448eE2641d78CC42D7AD76498917359D961A783" +# TODO: Change pytest.param(..., mark=pytest.mark.xfail) to the expectation/raises method +@pytest.mark.usefixtures("client", "web3") +class TestUniswap: # ------ Exchange ------------------------------------------------------------------ def test_get_fee_maker(self, client: Uniswap): + if client.version not in [1, 2]: + pytest.skip("Tested method not supported in this Uniswap version") r = client.get_fee_maker() assert r == 0 def test_get_fee_taker(self, client: Uniswap): + if client.version not in [1, 2]: + pytest.skip("Tested method not supported in this Uniswap version") r = client.get_fee_taker() assert r == 0.003 # ------ Market -------------------------------------------------------------------- @pytest.mark.parametrize( - "token, qty", + "token0", [ - (bat, ONE_ETH), - (dai, ONE_ETH), - (bat, 2 * ONE_ETH), - pytest.param("btc", ONE_ETH, marks=pytest.mark.xfail), + ("ETH"), + ("UNI"), + ("DAI"), + ("USDC"), ], ) - def test_get_eth_token_input_price(self, client, token, qty): - r = client.get_eth_token_input_price(token, qty) + def test_approve(self, client: Uniswap, tokens, token0): + token0 = tokens[token0] + if client.version == 1 and ETH_ADDRESS == token0: + pytest.skip("Not supported in this version of Uniswap") + r = client.approve(token0) assert r @pytest.mark.parametrize( - "token, qty", + "token0, token1, qty", [ - (bat, ONE_ETH), - (dai, ONE_ETH), - (bat, 2 * ONE_ETH), - pytest.param("btc", ONE_ETH, marks=pytest.mark.xfail), + ("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_token_eth_input_price(self, client, token, qty): - r = client.get_token_eth_input_price(token, qty) + 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, fee=FeeTier.TIER_3000) assert r @pytest.mark.parametrize( "token0, token1, qty", [ - (bat, dai, ONE_ETH), - (dai, bat, ONE_ETH), - (bat, dai, 2 * ONE_ETH), - (weth, dai, ONE_ETH), - (dai, weth, ONE_ETH), + ("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_token_token_input_price(self, client, token0, token1, qty): - if not client.version == 2: - pytest.skip("Tested method only supported on Uniswap v2") - r = client.get_token_token_input_price(token0, token1, qty) + def test_get_price_output(self, client: Uniswap, tokens, token0, token1, qty): + if client.version == 1 and "UNI" in (token0, token1): + pytest.xfail("Uniswap v1 UNI exchange has no liquidity — InvalidJump") + 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, fee=FeeTier.TIER_3000) + assert r + + @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: + pytest.skip("Only supported on Uniswap v2 and v3") + r = client.get_raw_price(token0, token1, fee=fee) assert r @pytest.mark.parametrize( - "token, qty", + "token0, token1, kwargs", [ - (bat, ONE_ETH), - (dai, ONE_ETH), - (bat, 2 * ONE_ETH), - pytest.param("btc", ONE_ETH, marks=pytest.mark.xfail), + ("WETH", "DAI", {"fee": FeeTier.TIER_3000}), ], ) - def test_get_eth_token_output_price(self, client, token, qty): - r = client.get_eth_token_output_price(token, qty) + def test_get_pool_instance(self, client, tokens, token0, token1, kwargs): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + r = client.get_pool_instance(token0, token1, **kwargs) assert r @pytest.mark.parametrize( - "token, qty", + "token0, token1, kwargs", [ - (bat, ONE_ETH), - (dai, ONE_ETH), - (bat, 2 * ONE_ETH), - pytest.param("btc", ONE_ETH, marks=pytest.mark.xfail), + ("WETH", "DAI", {"fee": FeeTier.TIER_3000}), ], ) - def test_get_token_eth_output_price(self, client, token, qty): - r = client.get_token_eth_output_price(token, qty) + def test_get_pool_immutables(self, client, tokens, token0, token1, kwargs): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + pool = client.get_pool_instance(token0, token1, **kwargs) + r = client.get_pool_immutables(pool) + print(r) assert r @pytest.mark.parametrize( - "token0, token1, qty", + "token0, token1, kwargs", [ - (bat, dai, ONE_ETH), - (dai, bat, ONE_ETH), - (bat, dai, 2 * ONE_ETH), - (weth, dai, ONE_ETH), - (dai, weth, ONE_ETH), + ("WETH", "DAI", {"fee": FeeTier.TIER_3000}), ], ) - def test_get_token_token_output_price(self, client, token0, token1, qty): - if not client.version == 2: - pytest.skip("Tested method only supported on Uniswap v2") - r = client.get_token_token_output_price(token0, token1, qty) + def test_get_pool_state(self, client, tokens, token0, token1, kwargs): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + pool = client.get_pool_instance(token0, token1, **kwargs) + r = client.get_pool_state(pool) + print(r) + assert r + + @pytest.mark.parametrize( + "amount0, amount1, token0, token1, kwargs", + [ + (1, 10, "WETH", "DAI", {"fee": FeeTier.TIER_3000}), + ], + ) + def test_mint_position( + self, client, tokens, amount0, amount1, token0, token1, kwargs + ): + token0, token1 = tokens[token0], tokens[token1] + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + pool = client.get_pool_instance(token0, token1, **kwargs) + r = client.mint_position(pool, amount0, amount1) + print(r) assert r # ------ ERC20 Pool ---------------------------------------------------------------- - @pytest.mark.parametrize("token", [(bat), (dai)]) + @pytest.mark.parametrize("token", [("UNI"), ("DAI")]) def test_get_ex_eth_balance( - self, client: Uniswap, token, + self, + client: Uniswap, + tokens, + token, ): - if not client.version == 1: - pytest.skip("Tested method only supported on Uniswap v1") - r = client.get_ex_eth_balance(token) + if client.version != 1: + pytest.skip("Only supported on Uniswap v1") + r = client.get_ex_eth_balance(tokens[token]) assert r - @pytest.mark.parametrize("token", [(bat), (dai)]) + @pytest.mark.parametrize("token", [("UNI"), ("DAI")]) def test_get_ex_token_balance( - self, client: Uniswap, token, + self, + client: Uniswap, + tokens, + token, ): - if not client.version == 1: - pytest.skip("Tested method only supported on Uniswap v1") - r = client.get_ex_token_balance(token) + if client.version != 1: + pytest.skip("Only supported on Uniswap v1") + r = client.get_ex_token_balance(tokens[token]) assert r - @pytest.mark.parametrize("token", [(bat), (dai)]) - def get_exchange_rate( - self, client: Uniswap, token, + @pytest.mark.parametrize("token", [("UNI"), ("DAI")]) + def test_get_exchange_rate( + self, + client: Uniswap, + tokens, + token, ): - r = client.get_exchange_rate(token) + if client.version != 1: + pytest.skip("Only supported on Uniswap v1") + r = client.get_exchange_rate(tokens[token]) assert r # ------ Liquidity ----------------------------------------------------------------- + @pytest.mark.parametrize( + "token0, token1, amount0, amount1, qty, fee", + [ + ("DAI", "USDC", ONE_ETH, ONE_USDC, ONE_ETH, FeeTier.TIER_3000), + ], + ) + def test_v3_deploy_pool_with_liquidity( + self, client: Uniswap, tokens, token0, token1, amount0, amount1, qty, fee + ): + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + + try: + pool = client.create_pool_instance(tokens[token0], tokens[token1], fee) + except Exception: + pool = client.get_pool_instance(tokens[token0], tokens[token1], fee) + + print(pool.address) + # Ensuring client has sufficient balance of both tokens + eth_to_dai = client.make_trade( + 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, + fee=fee, + ) + dai_to_usdc_tx = client.w3.eth.wait_for_transaction_receipt( + dai_to_usdc, timeout=RECEIPT_TIMEOUT + ) + assert dai_to_usdc_tx["status"] + + balance_0 = client.get_token_balance(tokens[token0]) + balance_1 = client.get_token_balance(tokens[token1]) + + assert balance_0 > amount0, f"Have: {balance_0} need {amount0}" + assert balance_1 > amount1, f"Have: {balance_1} need {amount1}" + + min_tick, max_tick = default_tick_range(fee) + r = client.mint_liquidity( + pool, + amount0, + amount1, + tick_lower=min_tick, + tick_upper=max_tick, + deadline=2**64, + ) + assert r["status"] + + position_balance = client.nonFungiblePositionManager.functions.balanceOf( + _addr_to_str(client.address) + ).call() + assert position_balance > 0 + + position_array = client.get_liquidity_positions() + assert len(position_array) > 0 + + @pytest.mark.parametrize( + "deadline", + [(2**64)], + ) + def test_close_position(self, client: Uniswap, deadline): + if client.version != 3: + pytest.skip("Not supported in this version of Uniswap") + position_array = client.get_liquidity_positions() + tokenId = position_array[0] + r = client.close_position(tokenId, deadline=deadline) + assert r["status"] + + @pytest.mark.parametrize("token0, token1", [("DAI", "USDC")]) + 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], fee=FeeTier.TIER_3000 + ) + tvl_0, tvl_1 = client.get_tvl_in_pool(pool) + assert tvl_0 > 0 + assert tvl_1 > 0 + @pytest.mark.skip @pytest.mark.parametrize( "token, max_eth", [ - (bat, 0.00001 * ONE_ETH), - (dai, 0.00001 * ONE_ETH), - pytest.param("btc", ONE_ETH, marks=pytest.mark.xfail), + ("UNI", 0.00001 * ONE_ETH), + ("DAI", 0.00001 * ONE_ETH), ], ) - def test_add_liquidity(self, client: Uniswap, web3: Web3, token, max_eth): + def test_add_liquidity(self, client: Uniswap, tokens, web3: Web3, token, max_eth): + token = tokens[token] r = client.add_liquidity(token, max_eth) - tx = web3.eth.waitForTransactionReceipt(r, timeout=6000) - assert tx.status # type: ignore + tx = web3.eth.wait_for_transaction_receipt(r, timeout=RECEIPT_TIMEOUT) + assert tx["status"] @pytest.mark.skip @pytest.mark.parametrize( "token, max_token, expectation", [ - (bat, 0.00001 * ONE_ETH, does_not_raise()), - (dai, 0.00001 * ONE_ETH, does_not_raise()), - ("btc", ONE_ETH, pytest.raises(InvalidToken)), + ("UNI", 0.00001 * ONE_ETH, does_not_raise()), + ("DAI", 0.00001 * ONE_ETH, does_not_raise()), ], ) def test_remove_liquidity( - self, client: Uniswap, web3: Web3, token, max_token, expectation + self, client: Uniswap, web3: Web3, tokens, token, max_token, expectation ): + token = tokens[token] with expectation: - r = client.remove_liquidity(token, max_token) - tx = web3.eth.waitForTransactionReceipt(r) - assert tx.status # type: ignore + r = client.remove_liquidity(tokens[token], max_token) + tx = web3.eth.wait_for_transaction_receipt(r) + assert tx["status"] # ------ Make Trade ---------------------------------------------------------------- @pytest.mark.parametrize( "input_token, output_token, qty, recipient, expectation", [ # ETH -> Token - (eth, bat, 1_000_000_000 * ONE_WEI, None, does_not_raise), + ("ETH", "DAI", ONE_ETH, None, does_not_raise), # Token -> Token - (dai, bat, 1_000_000_000 * ONE_WEI, None, does_not_raise), + ("DAI", "USDC", ONE_ETH, None, does_not_raise), # Token -> ETH - (bat, eth, 1_000_000 * ONE_WEI, None, does_not_raise), - # (eth, bat, 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), - # (bat, eth, 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), - # (dai, bat, 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), - (dai, "btc", ONE_ETH, None, lambda: pytest.raises(InvalidToken)), + ("USDC", "ETH", ONE_USDC // 2, None, does_not_raise), + # ("ETH", "UNI", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), + # ("UNI", "ETH", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), + # ("DAI", "UNI", 0.00001 * ONE_ETH, ZERO_ADDRESS, does_not_raise), ], ) def test_make_trade( self, client: Uniswap, web3: Web3, + tokens, + test_assets, input_token, output_token, qty: int, recipient, expectation, ): + input_token, output_token = tokens[input_token], tokens[output_token] + if client.version == 1 and ETH_ADDRESS not in [input_token, output_token]: + pytest.skip( + "Not supported in this version of Uniswap, or at least no liquidity" + ) + # Uniswap v1 token-to-ETH uses Vyper 0.1.x bytecode with non-standard JUMP + # patterns that Ganache tolerated but Anvil's strict revm rejects (InvalidJump). + # xfail until v1 is formally deprecated or a compatible fork-mode is found. + if client.version == 1 and output_token == ETH_ADDRESS: + pytest.xfail( + "v1 token-to-ETH: EvmError: InvalidJump — Vyper 0.1.x bytecode " + "incompatible with Anvil revm; tracked for v1 deprecation" + ) with expectation(): bal_in_before = client.get_token_balance(input_token) - r = client.make_trade(input_token, output_token, qty, recipient) - tx = web3.eth.waitForTransactionReceipt(r) - assert tx.status # type: ignore + 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}" # TODO: Checks for ETH, taking gas into account bal_in_after = client.get_token_balance(input_token) - if input_token != self.eth: + if input_token != tokens["ETH"]: assert bal_in_before - qty == bal_in_after @pytest.mark.parametrize( "input_token, output_token, qty, recipient, expectation", [ # ETH -> Token - (eth, bat, 1_000_000_000 * ONE_WEI, None, does_not_raise), + ("ETH", "DAI", ONE_ETH, None, does_not_raise), # Token -> Token - (bat, dai, 1_000_000_000 * ONE_WEI, None, does_not_raise), + ("DAI", "USDC", ONE_USDC, None, does_not_raise), # Token -> ETH - (dai, eth, 1_000_000 * ONE_WEI, None, does_not_raise), + ("DAI", "ETH", ONE_ETH // 10, None, does_not_raise), # FIXME: These should probably be uncommented eventually - # (eth, bat, int(0.000001 * ONE_ETH), ZERO_ADDRESS), - # (bat, eth, int(0.000001 * ONE_ETH), ZERO_ADDRESS), - # (dai, bat, int(0.000001 * ONE_ETH), ZERO_ADDRESS), - ( - dai, - eth, - 10 ** 18 * ONE_WEI, - None, - lambda: pytest.raises(InsufficientBalance), - ), - (dai, "btc", ONE_ETH, None, lambda: pytest.raises(InvalidToken)), + # ("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", "DAI", ONE_USDC, None, lambda: pytest.raises(ValueError)), ], ) def test_make_trade_output( self, client: Uniswap, web3: Web3, + tokens, + test_assets, input_token, output_token, qty: int, recipient, expectation, ): + input_token, output_token = tokens[input_token], tokens[output_token] + if client.version == 1 and ETH_ADDRESS not in [input_token, output_token]: + pytest.skip( + "Not supported in this version of Uniswap, or at least no liquidity" + ) + # Same Anvil revm InvalidJump for v1 token-to-ETH (see test_make_trade above). + if client.version == 1 and output_token == ETH_ADDRESS: + pytest.xfail( + "v1 token-to-ETH: EvmError: InvalidJump — Vyper 0.1.x bytecode " + "incompatible with Anvil revm; tracked for v1 deprecation" + ) with expectation(): balance_before = client.get_token_balance(output_token) - r = client.make_trade_output(input_token, output_token, qty, recipient) - tx = web3.eth.waitForTransactionReceipt(r, timeout=30) - assert tx.status # type: ignore + 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 != self.eth: + 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) diff --git a/tests/test_uniswap4.py b/tests/test_uniswap4.py new file mode 100644 index 00000000..d8c2d24a --- /dev/null +++ b/tests/test_uniswap4.py @@ -0,0 +1,1842 @@ +import logging +import os +import shutil +import socket +import subprocess +import time +from collections.abc import Generator +from contextlib import contextmanager +from dataclasses import astuple, dataclass + +import pytest +from web3 import Web3 +from web3.types import Nonce + +from uniswap import Uniswap4 +from uniswap.constants import ( + ETH_ADDRESS, + ZERO_HOOK, + universal_router_commands, + v4_actions, +) +from uniswap.types import AddressLike, PoolKey +from uniswap.util import V4pools, _addr_to_str, _str_to_addr + +pytestmark = pytest.mark.skipif( + os.getenv("UNISWAP_VERSION") != "4", + reason="This test file is for Uniswap v4. For Uniswap v1, v2, and v3 tests, see test_uniswap.py", +) + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + +RECEIPT_TIMEOUT = 5 + +ONE_ETH = 10**18 +ONE_USDT = 10**6 +ONE_USDC = 10**6 +ETH_DECIMALS = 18 +USDT_DECIMALS = 6 +USDC_DECIMALS = 6 +USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" +USDT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7" +ETH_USDC_FEE = 500 +ETH_USDC_TICK_SPACING = 10 +USDC_USDT_FEE = 10 +USDC_USDT_TICK_SPACING = 1 + +TOKEN_ID = 1 + +eth_usdc_poolkey: PoolKey = PoolKey( + currency0=ETH_ADDRESS, # ETH + currency1=USDC_ADDRESS, # USDC + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + hooks=ZERO_HOOK, +) + + +usdc_usdt_poolkey: PoolKey = PoolKey( + currency0=USDC_ADDRESS, # USDC + currency1=USDT_ADDRESS, # USDT + fee=USDC_USDT_FEE, + tick_spacing=USDC_USDT_TICK_SPACING, + hooks=ZERO_HOOK, +) + + +@dataclass +class AnvilInstance: + provider: str + eth_address: str + eth_privkey: str + + +@pytest.fixture(scope="module") +def client(web3: Web3, anvil: AnvilInstance) -> Uniswap4: + return Uniswap4( + anvil.eth_address, + anvil.eth_privkey, + web3=web3, + gas_limit=500_000, + ) + + +@pytest.fixture(scope="module") +def pool_service(web3: Web3) -> V4pools: + return V4pools(web3) + + +@pytest.fixture(scope="module") +def web3(anvil: AnvilInstance) -> Web3: + w3 = Web3(Web3.HTTPProvider(anvil.provider, request_kwargs={"timeout": 30})) + if 1 != int(w3.net.version): + logger.warning("PROVIDER was not a mainnet provider, which the tests require") + return w3 + + +@pytest.fixture(scope="module") +def anvil() -> Generator[AnvilInstance, None, None]: + """Fixture that runs anvil which has forked off mainnet""" + if not shutil.which("anvil"): + raise ValueError("anvil was not found in PATH") + if "PROVIDER" not in os.environ: + raise ValueError( + "PROVIDER was not set, you need to set it to a mainnet provider (such as Infura) so that we can fork off our testnet" + ) + + port = 10998 + defaultGasPrice = 100_000_000_000 # 100 gwei + p = subprocess.Popen( + f"""anvil + --port {port} + --chain-id 1 + --fork-url {os.environ["PROVIDER"]} + --gas-price {defaultGasPrice} + """.replace("\n", " "), + shell=True, + ) + # Address #1 when anvil is run with `--wallet.seed test`, it starts with 1000 ETH + eth_address = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" + eth_privkey = "0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6" + # Poll until Anvil accepts connections. Fail immediately if the process + # exits (for example a genesis failure), rather than waiting out the deadline. + deadline = time.monotonic() + 60 + while time.monotonic() < deadline: + if p.poll() is not None: + raise RuntimeError( + f"Anvil exited before listening on port {port} (code {p.returncode})" + ) + try: + with socket.create_connection(("127.0.0.1", port), timeout=1): + break + except OSError: + time.sleep(0.5) + else: + p.kill() + p.wait() + raise RuntimeError(f"Anvil did not start on port {port} within 60s") + yield AnvilInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) + p.kill() + p.wait() + + +@contextmanager +def does_not_raise(): + yield + + +@pytest.mark.usefixtures("client", "web3") +class TestUniswap4: + test_token_id: int = 0 + test_mint_tx_hash: str = "" + test_tick: int = 0 + + # ------ Approve/tx replacement----------------------------------------------------- + @pytest.mark.parametrize( + "token, max_approval, delay_interval", + [ + (USDC_ADDRESS, None, 7), + (USDT_ADDRESS, 1_000_000 * ONE_USDT, 1), + ], + ) + def test_approve( + self, + client: Uniswap4, + token: str, + max_approval: int | None, + delay_interval: int, + ): + # Approve the token + tx = client.approve( + _str_to_addr(token), + max_approval, + delay_interval=delay_interval, + approve_position_manager=True, + ) + assert tx + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + # Check that the approval was successful by calling allowance + allowance = client.approval(_str_to_addr(token)) + assert allowance + + @pytest.mark.parametrize( + "address_to, gas_price, priority_fee, custom_nonce", + [ + ("self", 20, 10, None), + (ETH_ADDRESS, 20, 10, 0), + ], + ) + def test_drop_txn( + self, + client: Uniswap4, + address_to: str, + gas_price: float, + priority_fee: int, + custom_nonce: int | None, + ): + if not client.w3.is_address(address_to): + address: AddressLike = client.address + else: + address = _str_to_addr(address_to) + client.update_last_nonce() + if custom_nonce == 0: + nonce: Nonce | None = client.last_nonce + else: + nonce = None + + tx = client.drop_txn( + address, + gas_price, + priority_fee=priority_fee, + custom_nonce=nonce, + ) + assert tx + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + client.update_last_nonce() + + # ------ Market/price impact-------------------------------------------------------- + # Input quotes + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + def test_get_quote_exact_input_single( + self, + client: Uniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = client.get_quote_exact_input_single( + token0, token1, qty, fee, tick_spacing, hooks + ) + assert result + + @pytest.mark.parametrize( + "token0, qty, route", + [ + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + ], + ), + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + ), + ], + ) + def test_get_quote_exact_input( + self, + client: Uniswap4, + token0: str, + qty: int, + route: list[PoolKey], + ): + result = client.get_quote_exact_input(token0, qty, route) + assert result + + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, route", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + ONE_ETH, + None, + None, + None, + None, + [eth_usdc_poolkey, usdc_usdt_poolkey], + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDT, + None, + None, + None, + None, + [usdc_usdt_poolkey, eth_usdc_poolkey], + ), + ], + ) + def test_get_price_input( + self, + client: Uniswap4, + token0: str, + token1: str, + qty: int, + fee: int | None, + tick_spacing: int | None, + hooks: str | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + ): + result = client.get_price_input( + token0, token1, qty, fee, tick_spacing, hooks, hook_data, route + ) + assert result + + # Output quotes + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + def test_get_quote_exact_output_single( + self, + client: Uniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = client.get_quote_exact_output_single( + token0, token1, qty, fee, tick_spacing, hooks + ) + assert result + + @pytest.mark.parametrize( + "token0, qty, route", + [ + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + ], + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + ), + ], + ) + def test_get_quote_exact_output( + self, client: Uniswap4, token0: str, qty: int, route: list[PoolKey] + ): + result = client.get_quote_exact_output(token0, qty, route) + assert result + + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, route", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + 1000 * ONE_USDT, + None, + None, + None, + None, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + None, + None, + None, + None, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + ), + ], + ) + def test_get_price_output( + self, + client: Uniswap4, + token0: str, + token1: str, + qty: int, + fee: int | None, + tick_spacing: int | None, + hooks: str | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + ): + result = client.get_price_output( + token0, token1, qty, fee, tick_spacing, hooks, hook_data, route + ) + assert result + + # Price impact/spot price + @pytest.mark.parametrize( + "token0, token1, test_volume", + [ + (ETH_ADDRESS, USDC_ADDRESS, ONE_ETH), + (ETH_ADDRESS, USDC_ADDRESS, 10 * ONE_ETH), + (ETH_ADDRESS, USDC_ADDRESS, 100 * ONE_ETH), + (ETH_ADDRESS, USDC_ADDRESS, 1000 * ONE_ETH), + ], + ) + def test_estimate_price_impact( + self, client: Uniswap4, token0: str, token1: str, test_volume: int + ): + result = client.estimate_price_impact(token0, token1, test_volume) + assert result + + @pytest.mark.parametrize( + "token0, token1", + [ + (ETH_ADDRESS, USDC_ADDRESS), + (USDC_ADDRESS, ETH_ADDRESS), + ], + ) + def test_get_token_token_spot_price( + self, client: Uniswap4, token0: str, token1: str + ): + result = client.get_token_token_spot_price(token0, token1) + assert result + + # ------ Swaps---------------------------------------------------------------------- + # Input swaps + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ], + ) + def test_token_to_token_swap_exact_input( + self, + client: Uniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + qtycap = client.get_quote_exact_input_single( + token0, token1, qty, fee, tick_spacing, hooks, hook_data + ) + tx = client.token_to_token_swap_exact_input( + token0, + qty, + qtycap, + token1, + fee, + tick_spacing, + hooks, + hook_data, + 0, # min_hop_price_x_36 + custom_nonce, + ) + assert tx + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, qty, route, custom_nonce", + [ + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + ], + None, + ), + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + None, + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + None, + ), + ], + ) + def test_token_to_token_swap_input( + self, + client: Uniswap4, + token0: str, + qty: int, + route: list[PoolKey], + custom_nonce: Nonce | None, + ): + qtycap = client.get_quote_exact_input(token0, qty, route) + + tx = client.token_to_token_swap_input( + token0, qty, qtycap, route, min_hop_price_x_36=[], custom_nonce=custom_nonce + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, token1, qty, pool_key, hook_data, route, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 10 * ONE_ETH, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDC, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + 10 * ONE_ETH, + None, + None, + [eth_usdc_poolkey, usdc_usdt_poolkey], + None, + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + 1000 * ONE_USDT, + None, + None, + [usdc_usdt_poolkey, eth_usdc_poolkey], + None, + ), + ], + ) + def test_make_swap_input( + self, + client: Uniswap4, + token0: str, + token1: str, + qty: int, + pool_key: PoolKey | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + custom_nonce: Nonce | None, + ): + qtycap = client.get_price_input( + token0, + token1, + qty, + None if pool_key is None else pool_key.fee, + None if pool_key is None else pool_key.tick_spacing, + None if pool_key is None else pool_key.hooks, + hook_data, + route, + ) + tx = client.make_swap_input( + token0, token1, qty, qtycap, pool_key, hook_data, route, custom_nonce + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # Output swaps + @pytest.mark.parametrize( + "token0, token1, qty, fee, tick_spacing, hooks, hook_data, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + ONE_USDC, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + b"", + None, + ), + ], + ) + def test_token_to_token_swap_exact_output( + self, + client: Uniswap4, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + qtycap = client.get_quote_exact_output_single( + token0, token1, qty, fee, tick_spacing, hooks, hook_data + ) + tx = client.token_to_token_swap_exact_output( + token0, + qty, + qtycap, + token1, + fee, + tick_spacing, + hooks, + hook_data, + 0, # min_hop_price_x_36 + custom_nonce, + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, qty, route, custom_nonce", + [ + ( + USDC_ADDRESS, + 1000 * ONE_USDC, + [ + eth_usdc_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + usdc_usdt_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + 1000 * ONE_USDT, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + None, + ), + ( + ETH_ADDRESS, + ONE_ETH, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + None, + ), + ], + ) + def test_token_to_token_swap_output( + self, + client: Uniswap4, + token0: str, + qty: int, + route: list[PoolKey], + custom_nonce: Nonce | None, + ): + qtycap = client.get_quote_exact_output(token0, qty, route) + tx = client.token_to_token_swap_output( + token0, qty, qtycap, route, min_hop_price_x_36=[], custom_nonce=custom_nonce + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "token0, token1, qty, pool_key, hook_data, route, custom_nonce", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + 1000 * ONE_USDC, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + USDC_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + eth_usdc_poolkey, + b"", + None, + None, + ), + ( + ETH_ADDRESS, + USDT_ADDRESS, + 1000 * ONE_USDT, + None, + None, + [ + eth_usdc_poolkey, + usdc_usdt_poolkey, + ], + None, + ), + ( + USDT_ADDRESS, + ETH_ADDRESS, + ONE_ETH, + None, + None, + [ + usdc_usdt_poolkey, + eth_usdc_poolkey, + ], + None, + ), + ], + ) + def test_make_swap_output( + self, + client: Uniswap4, + token0: str, + token1: str, + qty: int, + pool_key: PoolKey | None, + hook_data: bytes | None, + route: list[PoolKey] | None, + custom_nonce: Nonce | None, + ): + qtycap = client.get_price_output( + token0, + token1, + qty, + None if pool_key is None else pool_key.fee, + None if pool_key is None else pool_key.tick_spacing, + None if pool_key is None else pool_key.hooks, + hook_data, + route, + ) + tx = client.make_swap_output( + token0, token1, qty, qtycap, pool_key, hook_data, route, custom_nonce + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # ------ Liquidity -------------------------------------------------------------------- + + @pytest.mark.parametrize( + "pool_key, custom_nonce", + [ + (eth_usdc_poolkey, None), + ], + ) + def test_create_pool( + self, + client: Uniswap4, + pool_key: PoolKey, + custom_nonce: Nonce | None, + ): + sqrt_price_x96 = 1 << 96 # 1:1 price + test_pool_key = PoolKey( + currency0=pool_key.currency0, + currency1=pool_key.currency1, + fee=pool_key.fee + 100, + tick_spacing=pool_key.tick_spacing + 500, + hooks=pool_key.hooks, + ) + tx = client.create_pool(test_pool_key, sqrt_price_x96, custom_nonce) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "pool_key, tick_lower, tick_upper, liquidity, amount0, amount1, hook_data, custom_nonce", + [ + ( + eth_usdc_poolkey, + -600, + 600, + ONE_ETH, + ONE_ETH, + 2500 * ONE_USDC, + b"", + None, + ), + ], + ) + def test_mint_position( + self, + client: Uniswap4, + pool_key: PoolKey, + tick_lower: int, + tick_upper: int, + liquidity: int, + amount0: int, + amount1: int, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = client.mint_position( + pool_key, + tick_lower, + tick_upper, + liquidity, + amount0, + amount1, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + test_token_id_result = client.get_minted_token_id( + tx_receipt["transactionHash"].hex() + ) + assert len(test_token_id_result) > 0 + TestUniswap4.test_token_id = test_token_id_result[0] + TestUniswap4.test_mint_tx_hash = tx_receipt["transactionHash"].hex() + + def test_get_minted_token_id( + self, + client: Uniswap4, + ): + result = client.get_minted_token_id(TestUniswap4.test_mint_tx_hash) + assert result + + def test_get_position_info( + self, + client: Uniswap4, + ): + result = client.get_position_info(TestUniswap4.test_token_id) + test_pool_id_result: int = int.from_bytes(result["poolID"], byteorder="big") + truncated_pool_id_str = f"{test_pool_id_result:x}".lower() + pool_id_str = ( + client.get_pool_id( + PoolKey( + result["currency0"], + result["currency1"], + result["fee"], + result["tickSpacing"], + result["hooks"], + ) + ) + .hex() + .lower() + ) + assert truncated_pool_id_str == pool_id_str[: len(truncated_pool_id_str)] + + @pytest.mark.parametrize( + "token0_decimals, token1_decimals", + [(ETH_DECIMALS, USDC_DECIMALS)], + ) + def test_get_position_value( + self, + client: Uniswap4, + token0_decimals: int, + token1_decimals: int, + ): + result = client.get_position_value( + TestUniswap4.test_token_id, token0_decimals, token1_decimals + ) + assert result + + @pytest.mark.parametrize( + "pool_key, liquidity, amount0, amount1, hook_data, custom_nonce", + [ + ( + eth_usdc_poolkey, + int(0.05 * ONE_ETH), + int(0.5 * ONE_ETH), + 1250 * ONE_USDC, + b"", + None, + ), + ], + ) + def test_increase_liquidity( + self, + client: Uniswap4, + pool_key: PoolKey, + liquidity: int, + amount0: int, + amount1: int, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = client.increase_liquidity( + pool_key, + TestUniswap4.test_token_id, + amount0, + amount1, + liquidity, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "pool_key, hook_data, custom_nonce", + [ + (eth_usdc_poolkey, b"", None), + ], + ) + def test_collect_fees( + self, + client: Uniswap4, + pool_key: PoolKey, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = client.collect_fees( + pool_key, TestUniswap4.test_token_id, recipient, hook_data, custom_nonce + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + @pytest.mark.parametrize( + "pool_key, liquidity, amount0, amount1, hook_data, custom_nonce", + [ + ( + eth_usdc_poolkey, + int(0.05 * ONE_ETH), + 0, + 0, + b"", + None, + ), + ], + ) + def test_decrease_liquidity( + self, + client: Uniswap4, + pool_key: PoolKey, + liquidity: int, + amount0: int, + amount1: int, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + tx = client.decrease_liquidity( + pool_key, + TestUniswap4.test_token_id, + amount0, + amount1, + liquidity, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # ------ V4Pools tests ---------------------------------------------------------------- + @pytest.mark.parametrize( + "first_block", + [ + (21688329), + ], + ) + def test_fetch_poolkey_data( + self, + pool_service: V4pools, + first_block: int, + ): + result: int = pool_service.fetch_poolkey_data( + first_block, + chunk_size=5, + clear_list=False, + retry_attempts=1, + minutes_between_retries=1, + last_block=first_block + 101, + ) + if result != 0: + pytest.skip( + f"Failed to fetch poolkey data, connection issues, result: {result}" + ) + assert result == 0 + + @pytest.mark.parametrize( + "test_data_file_path", + [ + "pool_list.test", + ], + ) + def test_load_poolkeys_list( + self, + pool_service: V4pools, + test_data_file_path: str, + ): + test_data_file_full_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), test_data_file_path + ) + pool_service.load_poolkeys_list(test_data_file_full_path) + assert len(pool_service.poolkeys_list) > 0 + + @pytest.mark.parametrize( + "test_data_file_path", + [ + "pool_list_dump.test", + ], + ) + def test_save_poolkeys_list( + self, + pool_service: V4pools, + test_data_file_path: str, + tmp_path, + ): + test_data_file_full_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "pool_list.test" + ) + pool_service.load_poolkeys_list(test_data_file_full_path) + temp_dir = tmp_path / "temp" + temp_dir.mkdir() + test_data_dump_full_path = temp_dir / test_data_file_path + pool_service.save_poolkeys_list(test_data_dump_full_path) + assert len(test_data_dump_full_path.read_text()) > 0 + + @pytest.mark.parametrize( + "currecy0, currency1", + [ + (ETH_ADDRESS, USDC_ADDRESS), + (USDC_ADDRESS, USDT_ADDRESS), + ], + ) + def test_get_poolkeys_sublist( + self, + pool_service: V4pools, + currecy0: str, + currency1: str, + ): + test_data_file_full_path = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "pool_list.test" + ) + pool_service.load_poolkeys_list(test_data_file_full_path) + + result = pool_service.get_poolkeys_sublist(currecy0, currency1) + assert len(result) > 0 + + # ------ StateView tests -------------------------------------------------------------- + # Read methods + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + def test_stateview_get_liquidity( + self, + client: Uniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = client.stateview_get_liquidity( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + def test_stateview_get_slot0( + self, + client: Uniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = client.stateview_get_slot0( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + assert result + TestUniswap4.test_tick = int(result["tick"]) + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + def test_stateview_get_fee_growth_globals( + self, + client: Uniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = client.stateview_get_fee_growth_globals( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ( + USDC_ADDRESS, + USDT_ADDRESS, + USDC_USDT_FEE, + USDC_USDT_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + def test_stateview_get_fee_growth_inside( + self, + client: Uniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = client.stateview_get_fee_growth_inside( + token0, + token1, + fee, + tick_spacing, + hooks, + TestUniswap4.test_tick - tick_spacing * 50, + TestUniswap4.test_tick + tick_spacing * 50, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + def test_stateview_get_position_info( + self, + client: Uniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = client.stateview_get_position_info( + token0, + token1, + fee, + tick_spacing, + hooks, + _addr_to_str(client.address), + -600, + 600, + TestUniswap4.test_token_id, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + def test_stateview_get_tick_bitmap( + self, + client: Uniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = client.stateview_get_tick_bitmap( + token0, + token1, + fee, + tick_spacing, + hooks, + -30000, + ) + assert isinstance(result, int) + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + def test_stateview_get_tick_fee_growth_outside( + self, + client: Uniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = client.stateview_get_tick_fee_growth_outside( + token0, + token1, + fee, + tick_spacing, + hooks, + TestUniswap4.test_tick, + ) + assert result + + @pytest.mark.parametrize( + "token0, token1, fee, tick_spacing, hooks", + [ + ( + ETH_ADDRESS, + USDC_ADDRESS, + ETH_USDC_FEE, + ETH_USDC_TICK_SPACING, + ZERO_HOOK, + ), + ], + ) + def test_stateview_get_tick_pool_info( + self, + client: Uniswap4, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ): + result = client.stateview_get_tick_pool_info( + token0, + token1, + fee, + tick_spacing, + hooks, + TestUniswap4.test_tick, + ) + assert result + + # ------ PositionDescriptor tests ----------------------------------------------------- + # Read methods + @pytest.mark.parametrize( + "token0", + [ + (USDC_ADDRESS), + (USDT_ADDRESS), + ], + ) + def test_position_descriptor_get_currency_ratio_priority( + self, client: Uniswap4, token0: str + ): + result = client.position_descriptor_get_currency_ratio_priority(token0) + assert isinstance(result, int) + + @pytest.mark.parametrize( + "token0, token1", + [ + (USDC_ADDRESS, ETH_ADDRESS), + (USDT_ADDRESS, ETH_ADDRESS), + ], + ) + def test_position_descriptor_get_flip_ratio( + self, client: Uniswap4, token0: str, token1: str + ): + result = client.position_descriptor_get_flip_ratio(token0, token1) + assert isinstance(result, bool) + + def test_position_descriptor_get_native_currency_label(self, client: Uniswap4): + result = client.position_descriptor_get_native_currency_label() + assert result + + def test_position_descriptor_get_pool_manager(self, client: Uniswap4): + result = client.position_descriptor_get_pool_manager() + assert result + + def test_position_descriptor_get_token_uri(self, client: Uniswap4): + result = client.position_descriptor_get_token_uri( + _addr_to_str(client.position_manager_address), TestUniswap4.test_token_id + ) + assert result + + def test_position_descriptor_get_wrapped_native_address(self, client: Uniswap4): + result = client.position_descriptor_get_wrapped_native_address() + assert result + + # ------ PositionManager tests -------------------------------------------------------- + # Read methods + + # Write methods + + # ------ PoolManager tests ------------------------------------------------------------ + # Read methods + + # Write methods + + # Burn test position. + @pytest.mark.parametrize( + "pool_key, hook_data, custom_nonce", + [ + (eth_usdc_poolkey, b"", None), + ], + ) + def test_burn_position( + self, + client: Uniswap4, + pool_key: PoolKey, + hook_data: bytes, + custom_nonce: Nonce | None, + ): + recipient = _addr_to_str(client.address) + # Removing liquidity before burning position, otherwise burn will revert since position is not empty + tx = client.decrease_liquidity( + pool_key, + TestUniswap4.test_token_id, + 0, + 0, + client.stateview_get_position_info( + pool_key.currency0, + pool_key.currency1, + pool_key.fee, + pool_key.tick_spacing, + pool_key.hooks, + recipient, + -600, + 600, + TestUniswap4.test_token_id, + )["liquidity"], + recipient, + hook_data, + custom_nonce, + ) + assert tx + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # Now burn the position + tx = client.burn_position( + pool_key, + TestUniswap4.test_token_id, + 0, + 0, + recipient, + hook_data, + custom_nonce, + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + # ------ UniversalRouter tests -------------------------------------------------------- + def test_universal_router_execute_simple_command( + self, + client: Uniswap4, + ): + commands: list = [ + universal_router_commands["WRAP_ETH"], + ] + actions: list = [ + [], + ] + params: list = [ + [ + [ + _addr_to_str(client.address), + 1 * ONE_ETH, + ], + ], + ] + tx = client.universal_router_execute( + commands, actions, params, ether_amount=1 * ONE_ETH + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + def test_universal_router_execute_composite_command( + self, + client: Uniswap4, + ): + if eth_usdc_poolkey.currency0.lower() < eth_usdc_poolkey.currency1.lower(): + zero_for_one: bool = True + else: + zero_for_one = False + qty: int = 1 * ONE_ETH + qtycap: int = client.get_quote_exact_input_single( + eth_usdc_poolkey.currency0, + eth_usdc_poolkey.currency1, + qty, + eth_usdc_poolkey.fee, + eth_usdc_poolkey.tick_spacing, + eth_usdc_poolkey.hooks, + b"", + ) + qtycap = int((1 - client.max_slippage) * qtycap) + + commands: list = [ + universal_router_commands["V4_SWAP"], + ] + actions: list = [ + [ + v4_actions["SWAP_EXACT_IN_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ] + + params: list = [ + [ + [ + ( + astuple(eth_usdc_poolkey), + zero_for_one, + qty, + qtycap, + 0, # min_hop_price_x_36 + b"", + ) + ], + [ + eth_usdc_poolkey.currency0, + qty, + ], + [ + eth_usdc_poolkey.currency1, + qtycap, + ], + ], + ] + tx = client.universal_router_execute( + commands, actions, params, ether_amount=qty + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) + + def test_universal_router_execute_multiaction( + self, + client: Uniswap4, + ): + if eth_usdc_poolkey.currency0.lower() < eth_usdc_poolkey.currency1.lower(): + zero_for_one: bool = True + else: + zero_for_one = False + qty: int = 1 * ONE_ETH + qtycap: int = client.get_quote_exact_input_single( + eth_usdc_poolkey.currency0, + eth_usdc_poolkey.currency1, + qty, + eth_usdc_poolkey.fee, + eth_usdc_poolkey.tick_spacing, + eth_usdc_poolkey.hooks, + b"", + ) + qtycap = int((1 - client.max_slippage) * qtycap) + ether_value = ONE_ETH + + # Executes a WRAP_ETH followed by a V4_SWAP in the same transaction + commands: list = [ + universal_router_commands["WRAP_ETH"], + universal_router_commands["V4_SWAP"], + ] + + # List of actions for each command, in this case we have 0 action for the WRAP_ETH command and 3 actions for the V4_SWAP command (swap, settle and take) + # As WRAP_ETH command does not require any action, we pass an empty list for it + actions: list = [ + [], + [ + v4_actions["SWAP_EXACT_IN_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ] + + # List of parameters for each action, the first element of the list corresponds to the parameters for the WRAP_ETH command (recipient and amount), + # and the second element corresponds to the parameters for each of the 3 actions of the V4_SWAP command + params: list = [ + [ + [ + _addr_to_str(client.address), + ether_value, + ], + ], + [ + [ + ( + astuple(eth_usdc_poolkey), + zero_for_one, + qty, + qtycap, + 0, # min_hop_price_x_36 + b"", + ) + ], + [ + eth_usdc_poolkey.currency0, + qty, + ], + [ + eth_usdc_poolkey.currency1, + qtycap, + ], + ], + ] + # Both commands use ether as input token, so we set the ether_amount to the sum of the ether required for each commands + tx = client.universal_router_execute( + commands, actions, params, ether_amount=(ether_value + qty) + ) + assert tx + + tx_receipt = client.w3.eth.wait_for_transaction_receipt( + tx, timeout=RECEIPT_TIMEOUT + ) + assert tx_receipt["status"], ( + f"Transaction failed with status {tx_receipt['status']}; tx_receipt: {tx_receipt}" + ) diff --git a/tests/test_v4_examples.py b/tests/test_v4_examples.py new file mode 100644 index 00000000..4ef4d019 --- /dev/null +++ b/tests/test_v4_examples.py @@ -0,0 +1,329 @@ +""" +Runnable examples for Uniswap v4. + +These tests serve as executable documentation: each test demonstrates one +common workflow against a live mainnet fork (Anvil). They run as part of the +``UNISWAP_VERSION=4`` CI matrix job, so every snippet is verified on every +commit. + +Run locally: + PROVIDER= UNISWAP_VERSION=4 pytest tests/test_v4_examples.py -v + +See docs/v4.rst for the prose version of these examples. +""" + +import os +import shutil +import subprocess +from collections.abc import Generator +from dataclasses import dataclass +from time import sleep + +import pytest +from web3 import Web3 + +from uniswap import Uniswap4 +from uniswap.constants import ETH_ADDRESS, ZERO_HOOK +from uniswap.types import PoolKey +from uniswap.util import V4pools + +pytestmark = pytest.mark.skipif( + os.getenv("UNISWAP_VERSION") != "4", + reason="Run with UNISWAP_VERSION=4", +) + +# --------------------------------------------------------------------------- +# Well-known mainnet addresses used in examples +# --------------------------------------------------------------------------- + +USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" +USDT_ADDRESS = "0xdAC17F958D2ee523a2206206994597C13D831ec7" + +# Canonical fee tiers and tick spacings +ETH_USDC_FEE = 500 +ETH_USDC_TICK_SPACING = 10 +USDC_USDT_FEE = 100 +USDC_USDT_TICK_SPACING = 1 + +ONE_ETH = 10**18 +ONE_USDC = 10**6 +ONE_USDT = 10**6 + +# PoolKey for ETH/USDC 0.05% +ETH_USDC_POOL = PoolKey( + currency0=ETH_ADDRESS, + currency1=USDC_ADDRESS, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + hooks=ZERO_HOOK, +) + +# PoolKey for USDC/USDT 0.01% +USDC_USDT_POOL = PoolKey( + currency0=USDC_ADDRESS, + currency1=USDT_ADDRESS, + fee=USDC_USDT_FEE, + tick_spacing=USDC_USDT_TICK_SPACING, + hooks=ZERO_HOOK, +) + + +# --------------------------------------------------------------------------- +# Fixtures — same pattern as test_uniswap4.py +# --------------------------------------------------------------------------- + + +@dataclass +class AnvilInstance: + provider: str + eth_address: str + eth_privkey: str + + +@pytest.fixture(scope="module") +def anvil() -> Generator[AnvilInstance, None, None]: + """Start an Anvil instance forked from mainnet.""" + if not shutil.which("anvil"): + raise ValueError("anvil was not found in PATH") + if "PROVIDER" not in os.environ: + raise ValueError( + "PROVIDER must be set to a mainnet RPC URL so Anvil can fork mainnet." + ) + + port = 10997 # different from test_uniswap4.py (10998) to allow parallel runs + p = subprocess.Popen( + f"anvil --port {port} --chain-id 1 --fork-url {os.environ['PROVIDER']}", + shell=True, + ) + # Anvil test account #2 (1000 ETH pre-funded) + eth_address = "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" + eth_privkey = "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" + sleep(3) + yield AnvilInstance(f"http://127.0.0.1:{port}", eth_address, eth_privkey) + p.kill() + p.wait() + + +@pytest.fixture(scope="module") +def web3(anvil: AnvilInstance) -> Web3: + return Web3(Web3.HTTPProvider(anvil.provider, request_kwargs={"timeout": 30})) + + +@pytest.fixture(scope="module") +def client(web3: Web3, anvil: AnvilInstance) -> Uniswap4: + return Uniswap4( + anvil.eth_address, + anvil.eth_privkey, + web3=web3, + gas_limit=500_000, + ) + + +@pytest.fixture(scope="module") +def pool_service(web3: Web3) -> V4pools: + return V4pools(web3) + + +# --------------------------------------------------------------------------- +# Example 1: Get the spot price +# --------------------------------------------------------------------------- + + +class TestExampleSpotPrice: + """Example: reading the current pool price from the StateView contract.""" + + def test_get_eth_usdc_spot_price(self, client: Uniswap4) -> None: + """ + get_token_token_spot_price returns the spot price of token0 in units + of token1. The result comes from the pool's sqrtPriceX96 slot without + executing any swap. + """ + price = client.get_token_token_spot_price( + token0=ETH_ADDRESS, + token1=USDC_ADDRESS, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + # Sanity-check: ETH is worth more than $100 and less than $1M + assert 100 < price < 1_000_000, f"Unexpected ETH/USDC price: {price}" + + def test_spot_price_is_reciprocal(self, client: Uniswap4) -> None: + """The USDC/ETH price should be roughly 1/ETH_USDC price.""" + eth_usdc = client.get_token_token_spot_price( + ETH_ADDRESS, USDC_ADDRESS, ETH_USDC_FEE, ETH_USDC_TICK_SPACING + ) + usdc_eth = client.get_token_token_spot_price( + USDC_ADDRESS, ETH_ADDRESS, ETH_USDC_FEE, ETH_USDC_TICK_SPACING + ) + assert abs(usdc_eth - 1.0 / eth_usdc) / (1.0 / eth_usdc) < 0.001 + + +# --------------------------------------------------------------------------- +# Example 2: Quote a trade before executing it +# --------------------------------------------------------------------------- + + +class TestExampleQuote: + """Example: use the on-chain Quoter to get an accurate trade estimate.""" + + def test_quote_exact_input_single_hop(self, client: Uniswap4) -> None: + """ + get_price_input returns the USDC received for a fixed ETH input. + Use this before a swap to set the slippage cap (qtycap). + """ + usdc_out = client.get_price_input( + token0=ETH_ADDRESS, + token1=USDC_ADDRESS, + qty=ONE_ETH, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + # Quote should be a positive integer (USDC in smallest unit) + assert usdc_out > 0 + usdc_human = usdc_out / ONE_USDC + assert 100 < usdc_human < 1_000_000, f"Unexpected USDC quote: {usdc_human}" + + def test_quote_exact_output_single_hop(self, client: Uniswap4) -> None: + """ + get_price_output returns the ETH required to buy an exact USDC amount. + """ + eth_needed = client.get_price_output( + token0=ETH_ADDRESS, + token1=USDC_ADDRESS, + qty=1000 * ONE_USDC, # buy 1000 USDC + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + assert eth_needed > 0 + assert eth_needed < ONE_ETH, "1000 USDC should cost less than 1 ETH" + + def test_quote_exact_input_two_hop(self, client: Uniswap4) -> None: + """ + Multi-hop quote: ETH → USDC → USDT via two pools. + Pass a list of PoolKey objects as ``route``. + """ + usdt_out = client.get_price_input( + token0=ETH_ADDRESS, + token1=USDT_ADDRESS, + qty=ONE_ETH // 10, # 0.1 ETH + route=[ETH_USDC_POOL, USDC_USDT_POOL], + ) + assert usdt_out > 0 + usdt_human = usdt_out / ONE_USDT + assert 10 < usdt_human < 100_000, f"Unexpected USDT quote: {usdt_human}" + + +# --------------------------------------------------------------------------- +# Example 3: Estimate price impact before swapping +# --------------------------------------------------------------------------- + + +class TestExamplePriceImpact: + """Example: check price impact to avoid trading into thin pools.""" + + def test_small_trade_has_low_impact(self, client: Uniswap4) -> None: + """ + A small ETH trade in a deep ETH/USDC pool should have near-zero impact. + estimate_price_impact returns a float: 0.01 = 1%. + """ + impact = client.estimate_price_impact( + token0=ETH_ADDRESS, + token1=USDC_ADDRESS, + qty=ONE_ETH // 100, # 0.01 ETH — tiny relative to pool depth + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + # Impact should be small (< 1%) for a tiny trade in a liquid pool + assert impact < 0.01, f"Expected low impact, got {impact:.4%}" + + def test_large_trade_has_higher_impact(self, client: Uniswap4) -> None: + """ + A larger trade should show measurably more price impact than a tiny one. + """ + small_impact = client.estimate_price_impact( + ETH_ADDRESS, + USDC_ADDRESS, + ONE_ETH // 100, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + large_impact = client.estimate_price_impact( + ETH_ADDRESS, + USDC_ADDRESS, + 100 * ONE_ETH, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + ) + assert large_impact > small_impact + + +# --------------------------------------------------------------------------- +# Example 4: Read pool state via StateView +# --------------------------------------------------------------------------- + + +class TestExampleStateView: + """Example: read on-chain pool state without making a swap.""" + + def test_get_slot0(self, client: Uniswap4) -> None: + """ + stateview_get_slot0 returns the current sqrtPriceX96 and tick for a pool. + This is the cheapest way to read the current price. + """ + slot0 = client.stateview_get_slot0( + ETH_ADDRESS, + USDC_ADDRESS, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + hooks=ZERO_HOOK, + ) + assert slot0["sqrtPriceX96"] > 0 + # Tick is a signed integer; ETH/USDC tick should be negative + # (ETH address < USDC address, so token0=ETH, current price ~3400 USDC + # corresponds to a negative tick) + assert isinstance(slot0["tick"], int) + + def test_get_pool_liquidity(self, client: Uniswap4) -> None: + """ + stateview_get_liquidity returns total in-range liquidity for a pool. + A popular pool like ETH/USDC 0.05% should always have positive liquidity. + """ + liquidity = client.stateview_get_liquidity( + ETH_ADDRESS, + USDC_ADDRESS, + fee=ETH_USDC_FEE, + tick_spacing=ETH_USDC_TICK_SPACING, + hooks=ZERO_HOOK, + ) + assert liquidity > 0, "ETH/USDC 0.05% pool should have liquidity" + + +# --------------------------------------------------------------------------- +# Example 5: Discover pools for a token pair +# --------------------------------------------------------------------------- + + +class TestExamplePoolDiscovery: + """Example: filter a saved v4 pool index by token pair using V4pools.""" + + def test_find_eth_usdc_pools(self, pool_service: V4pools) -> None: + """ + Load a checked-in pool index, then filter it by token pair. + + ``fetch_poolkey_data`` is documented in ``docs/v4.rst`` but is not + suitable for the public CI RPC: scanning historical logs requires an + archive endpoint. The fixture is generated by that method and keeps + this example deterministic while exercising the filtering workflow. + """ + pool_data = os.path.join(os.path.dirname(__file__), "pool_list.test") + pool_service.load_poolkeys_list(pool_data) + + eth_usdc_pools = pool_service.get_poolkeys_sublist(ETH_ADDRESS, USDC_ADDRESS) + # There should be at least one ETH/USDC pool (the 0.05% one) + assert len(eth_usdc_pools) >= 1 + + # All returned keys should reference the correct tokens + for pk in eth_usdc_pools: + currencies = {pk.currency0.lower(), pk.currency1.lower()} + assert ETH_ADDRESS.lower() in currencies + assert USDC_ADDRESS.lower() in currencies 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_adoption_snapshot.py b/tests/units/test_adoption_snapshot.py new file mode 100644 index 00000000..67455593 --- /dev/null +++ b/tests/units/test_adoption_snapshot.py @@ -0,0 +1,231 @@ +"""Unit tests for scripts/adoption_snapshot.py traffic reconstruction. + +No network. The grant D4 report must not subtract two rolling 14-day windows. +""" +from __future__ import annotations + +import importlib.util +from datetime import date, timedelta +from pathlib import Path +from typing import Any, Dict, List + +_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "adoption_snapshot.py" + + +def _mod() -> Any: + spec = importlib.util.spec_from_file_location("adoption_snapshot", _SCRIPT) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _daily(start: str, counts: List[int]) -> List[Dict[str, Any]]: + d = date.fromisoformat(start) + rows = [] + for n in counts: + rows.append({"date": d.isoformat(), "count": n, "uniques": 1}) + d += timedelta(days=1) + return rows + + +def _snap(day: str, clones: List[Dict[str, Any]], clones_14d: int) -> Dict[str, Any]: + return { + "snapshot_at": day + "T06:00:00+00:00", + "github": {"stars": 1000}, + "github_traffic": { + "clones_14d": clones_14d, + "unique_cloners_14d": 10, + "views_14d": clones_14d, + "unique_visitors_14d": 10, + "clones_daily": clones, + "views_daily": clones, + }, + "pypi": {}, + } + + +def test_reconstruct_unions_days_without_double_counting() -> None: + snap = _mod() + # Two adjacent 14-day windows. Naive subtraction of 14d totals is 80-100=-20. + a = _snap("2026-09-02", _daily("2026-08-20", [10] * 14), clones_14d=140) + b = _snap("2026-09-16", _daily("2026-09-03", [5] * 14), clones_14d=70) + # Period 09-02 → 09-16 = 09-02 (10) + 14 days of 5 = 80 + result = snap.reconstruct_period_counts( + [a, b], "2026-09-02", "2026-09-16", "clones_daily" + ) + assert result["total"] == 10 + 5 * 14 + assert result["days_covered"] == 15 + assert result["days_span"] == 15 + assert result["missing_days"] == [] + naive = 70 - 140 + assert result["total"] != naive + assert result["total"] > 0 + + +def test_reconstruct_takes_max_on_overlapping_same_day() -> None: + snap = _mod() + early = _snap( + "2026-09-02", + [{"date": "2026-09-02", "count": 3, "uniques": 1}], + clones_14d=3, + ) + later = _snap( + "2026-09-02", + [{"date": "2026-09-02", "count": 8, "uniques": 2}], + clones_14d=8, + ) + result = snap.reconstruct_period_counts( + [early, later], "2026-09-02", "2026-09-02", "clones_daily" + ) + assert result["total"] == 8 + + +def test_reconstruct_lists_gaps() -> None: + snap = _mod() + a = _snap( + "2026-09-02", + [{"date": "2026-09-02", "count": 1, "uniques": 1}], + clones_14d=1, + ) + b = _snap( + "2026-09-05", + [{"date": "2026-09-05", "count": 1, "uniques": 1}], + clones_14d=1, + ) + result = snap.reconstruct_period_counts( + [a, b], "2026-09-02", "2026-09-05", "clones_daily" + ) + assert result["total"] == 2 + assert result["missing_days"] == ["2026-09-03", "2026-09-04"] + + +def test_format_report_does_not_subtract_rolling_windows() -> None: + snap = _mod() + baseline = _snap("2026-09-02", _daily("2026-08-20", [10] * 14), clones_14d=140) + current = _snap("2026-09-16", _daily("2026-09-03", [5] * 14), clones_14d=70) + # Freeze now to the day *after* current so 2026-09-16 is a complete day. + report = snap.format_report( + baseline, current, snapshots=[baseline, current], now=date(2026, 9, 17) + ) + assert "-20" not in report + assert "-70" not in report + # Naive 14d delta would be 70-140. Reconstructed period clones = 80. + assert "| Clones | 80 |" in report + assert "not subtracted" in report + assert "Period totals (2026-09-02 → 2026-09-16)" in report + # 14d table has no Delta column + assert "Rolling 14-day windows (point-in-time, not a period delta)" in report + traffic_header = "### Rolling 14-day windows" + idx = report.index(traffic_header) + rolling = report[idx : idx + 600] + assert "Delta" not in rolling + + +def test_complete_period_end_drops_today() -> None: + snap = _mod() + closed, excluded = snap.complete_period_end("2026-09-16", now=date(2026, 9, 16)) + assert closed == "2026-09-15" + assert excluded == "2026-09-16" + closed, excluded = snap.complete_period_end("2026-09-16", now=date(2026, 9, 17)) + assert closed == "2026-09-16" + assert excluded is None + + +def test_complete_period_end_without_now_uses_snapshot_day() -> None: + """Production default must not re-read datetime.now(). + + If collection starts on 09-16 and midnight passes during network I/O, + a fresh clock would skip exclusion and publish 09-16's partial row. + """ + snap = _mod() + closed, excluded = snap.complete_period_end("2026-09-16") + assert closed == "2026-09-15" + assert excluded == "2026-09-16" + closed, excluded = snap.complete_period_end("not-a-date") + assert closed == "not-a-date" + assert excluded is None + + +def test_format_report_excludes_partial_current_utc_day() -> None: + snap = _mod() + baseline = _snap("2026-09-02", _daily("2026-08-20", [10] * 14), clones_14d=140) + current = _snap("2026-09-16", _daily("2026-09-03", [5] * 14), clones_14d=70) + report = snap.format_report( + baseline, current, snapshots=[baseline, current], now=date(2026, 9, 16) + ) + # 09-02 (10) + 09-03..09-15 (13*5) = 75; 09-16's partial 5 is dropped. + assert "| Clones | 75 |" in report + assert "Period totals (2026-09-02 → 2026-09-15)" in report + assert "2026-09-16 excluded" in report + assert "| Clones | 80 |" not in report + + +def test_format_report_midnight_crossing_excludes_collection_day() -> None: + """Wall clock after midnight must not resurrect the partial collection day. + + collect_metrics stamps snapshot_at first, then does network I/O. + format_report() with now=None (the production default) must key off + snapshot_at, not datetime.now(). + """ + snap = _mod() + baseline = _snap("2026-09-02", _daily("2026-08-20", [10] * 14), clones_14d=140) + current = _snap("2026-09-16", _daily("2026-09-03", [5] * 14), clones_14d=70) + report = snap.format_report(baseline, current, snapshots=[baseline, current]) + assert "| Clones | 75 |" in report + assert "Period totals (2026-09-02 → 2026-09-15)" in report + assert "2026-09-16 excluded" in report + assert "| Clones | 80 |" not in report + + +def test_format_report_baseline_today_has_no_complete_days() -> None: + snap = _mod() + baseline = _snap( + "2026-09-02", + [{"date": "2026-09-02", "count": 3, "uniques": 1}], + clones_14d=3, + ) + report = snap.format_report( + baseline, baseline, snapshots=[baseline], now=date(2026, 9, 2) + ) + assert "No complete UTC days" in report + assert "Period totals (" not in report + + +def test_format_report_without_daily_series_does_not_invent_delta() -> None: + snap = _mod() + baseline = { + "snapshot_at": "2026-09-02T00:00:00+00:00", + "github": {"stars": 1000, "forks": 1, "watchers": 1}, + "github_traffic": {"clones_14d": 100, "views_14d": 200}, + "pypi": {}, + } + current = { + "snapshot_at": "2026-11-01T00:00:00+00:00", + "github": {"stars": 1100, "forks": 2, "watchers": 1}, + "github_traffic": {"clones_14d": 40, "views_14d": 50}, + "pypi": {}, + } + report = snap.format_report(baseline, current, now=date(2026, 11, 2)) + assert "Period totals unavailable" in report + assert "-60" not in report # 40-100 + assert "-150" not in report # 50-200 + # Stars *are* cumulative; that delta is honest. + assert "+100" in report + + +def test_traffic_daily_series_normalizes_github_payload() -> None: + snap = _mod() + payload = { + "count": 5, + "uniques": 3, + "clones": [ + {"timestamp": "2026-09-01T00:00:00Z", "count": 2, "uniques": 1}, + {"timestamp": "2026-09-02T00:00:00Z", "count": 3, "uniques": 2}, + ], + } + rows = snap._traffic_daily_series(payload, "clones") + assert rows == [ + {"date": "2026-09-01", "count": 2, "uniques": 1}, + {"date": "2026-09-02", "count": 3, "uniques": 2}, + ] 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) diff --git a/uniswap/__init__.py b/uniswap/__init__.py index 6373a447..f010b393 100644 --- a/uniswap/__init__.py +++ b/uniswap/__init__.py @@ -1 +1,19 @@ -from .uniswap import Uniswap, InvalidToken, InsufficientBalance +from . import exceptions +from .asyncuniswap import AsyncUniswap +from .asyncuniswap4 import AsyncUniswap4 +from .cli import main +from .uniswap import Uniswap, _str_to_addr +from .uniswap4 import Uniswap4 +from .util import AsyncV4pools, V4pools + +__all__ = [ + "AsyncUniswap", + "AsyncUniswap4", + "AsyncV4pools", + "Uniswap", + "Uniswap4", + "V4pools", + "_str_to_addr", + "exceptions", + "main", +] diff --git a/uniswap/assets/erc20.abi b/uniswap/assets/erc20.abi index b3998741..4c2bf735 100644 --- a/uniswap/assets/erc20.abi +++ b/uniswap/assets/erc20.abi @@ -1,221 +1,263 @@ [ - { - "constant": false, - "inputs": [ - { - "name": "spender", - "type": "address" - }, - { - "name": "value", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "from", - "type": "address" - }, - { - "name": "to", - "type": "address" - }, - { - "name": "value", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "spender", - "type": "address" - }, - { - "name": "addedValue", - "type": "uint256" - } - ], - "name": "increaseAllowance", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "owner", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "spender", - "type": "address" - }, - { - "name": "subtractedValue", - "type": "uint256" - } - ], - "name": "decreaseAllowance", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "to", - "type": "address" - }, - { - "name": "value", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "name": "", - "type": "bool" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "name": "owner", - "type": "address" - }, - { - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "from", - "type": "address" - }, - { - "indexed": true, - "name": "to", - "type": "address" - }, - { - "indexed": false, - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - } -] \ No newline at end of file + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "string" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "from", + "type": "address" + }, + { + "name": "to", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "to", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "owner", + "type": "address" + }, + { + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } +] diff --git a/uniswap/assets/erc20b32.abi b/uniswap/assets/erc20b32.abi new file mode 100644 index 00000000..267eeb05 --- /dev/null +++ b/uniswap/assets/erc20b32.abi @@ -0,0 +1,263 @@ +[ + { + "constant": true, + "inputs": [], + "name": "name", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "symbol", + "outputs": [ + { + "name": "", + "type": "bytes32" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [ + { + "name": "", + "type": "uint8" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "from", + "type": "address" + }, + { + "name": "to", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "spender", + "type": "address" + }, + { + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "to", + "type": "address" + }, + { + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "name": "", + "type": "bool" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "name": "owner", + "type": "address" + }, + { + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "from", + "type": "address" + }, + { + "indexed": true, + "name": "to", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + } +] diff --git a/uniswap/assets/uniswap-v1/exchange.abi b/uniswap/assets/uniswap-v1/exchange.abi index 84b4abff..21fc06f9 100644 --- a/uniswap/assets/uniswap-v1/exchange.abi +++ b/uniswap/assets/uniswap-v1/exchange.abi @@ -1002,4 +1002,4 @@ "type": "function", "gas": 1713 } -] \ No newline at end of file +] diff --git a/uniswap/assets/uniswap-v2/factory.abi b/uniswap/assets/uniswap-v2/factory.abi index c34cba18..ad565068 100644 --- a/uniswap/assets/uniswap-v2/factory.abi +++ b/uniswap/assets/uniswap-v2/factory.abi @@ -1,181 +1,181 @@ [ - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "token0", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "token1", - "type": "address" - }, - { - "indexed": false, - "internalType": "address", - "name": "pair", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "PairCreated", - "type": "event" - }, - { - "constant": true, - "inputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "name": "allPairs", - "outputs": [ - { - "internalType": "address", - "name": "pair", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "allPairsLength", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "tokenA", - "type": "address" - }, - { - "internalType": "address", - "name": "tokenB", - "type": "address" - } - ], - "name": "createPair", - "outputs": [ - { - "internalType": "address", - "name": "pair", - "type": "address" - } - ], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "feeTo", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "feeToSetter", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [ - { - "internalType": "address", - "name": "tokenA", - "type": "address" - }, - { - "internalType": "address", - "name": "tokenB", - "type": "address" - } - ], - "name": "getPair", - "outputs": [ - { - "internalType": "address", - "name": "pair", - "type": "address" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "setFeeTo", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "name": "setFeeToSetter", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - } + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "pair", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "PairCreated", + "type": "event" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "allPairs", + "outputs": [ + { + "internalType": "address", + "name": "pair", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "allPairsLength", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + } + ], + "name": "createPair", + "outputs": [ + { + "internalType": "address", + "name": "pair", + "type": "address" + } + ], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "feeTo", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "feeToSetter", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + } + ], + "name": "getPair", + "outputs": [ + { + "internalType": "address", + "name": "pair", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "setFeeTo", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "setFeeToSetter", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + } ] diff --git a/uniswap/assets/uniswap-v3/factory.abi b/uniswap/assets/uniswap-v3/factory.abi new file mode 100644 index 00000000..63f8e7a5 --- /dev/null +++ b/uniswap/assets/uniswap-v3/factory.abi @@ -0,0 +1,236 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + } + ], + "name": "FeeAmountEnabled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnerChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "indexed": false, + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "name": "PoolCreated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenA", + "type": "address" + }, + { + "internalType": "address", + "name": "tokenB", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + } + ], + "name": "createPool", + "outputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + } + ], + "name": "enableFeeAmount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint24", + "name": "", + "type": "uint24" + } + ], + "name": "feeAmountTickSpacing", + "outputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint24", + "name": "", + "type": "uint24" + } + ], + "name": "getPool", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "parameters", + "outputs": [ + { + "internalType": "address", + "name": "factory", + "type": "address" + }, + { + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_owner", + "type": "address" + } + ], + "name": "setOwner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/uniswap/assets/uniswap-v3/multicall.abi b/uniswap/assets/uniswap-v3/multicall.abi new file mode 100644 index 00000000..2760624a --- /dev/null +++ b/uniswap/assets/uniswap-v3/multicall.abi @@ -0,0 +1,313 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "aggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes[]", + "name": "returnData", + "type": "bytes[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "blockAndAggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "name": "getBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBlockNumber", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockCoinbase", + "outputs": [ + { + "internalType": "address", + "name": "coinbase", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockDifficulty", + "outputs": [ + { + "internalType": "uint256", + "name": "difficulty", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "gaslimit", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getCurrentBlockTimestamp", + "outputs": [ + { + "internalType": "uint256", + "name": "timestamp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "addr", + "type": "address" + } + ], + "name": "getEthBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLastBlockHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryAggregate", + "outputs": [ + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "requireSuccess", + "type": "bool" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Call[]", + "name": "calls", + "type": "tuple[]" + } + ], + "name": "tryBlockAndAggregate", + "outputs": [ + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "blockHash", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "returnData", + "type": "bytes" + } + ], + "internalType": "struct Multicall2.Result[]", + "name": "returnData", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/uniswap/assets/uniswap-v3/nonFungiblePositionManager.abi b/uniswap/assets/uniswap-v3/nonFungiblePositionManager.abi new file mode 100644 index 00000000..5412fa6e --- /dev/null +++ b/uniswap/assets/uniswap-v3/nonFungiblePositionManager.abi @@ -0,0 +1,1221 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_factory", + "type": "address" + }, + { + "internalType": "address", + "name": "_WETH9", + "type": "address" + }, + { + "internalType": "address", + "name": "_tokenDescriptor_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "approved", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "Collect", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "DecreaseLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "IncreaseLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "WETH9", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "baseURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "amount0Max", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1Max", + "type": "uint128" + } + ], + "internalType": "struct INonfungiblePositionManager.CollectParams", + "name": "params", + "type": "tuple" + } + ], + "name": "collect", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + } + ], + "name": "createAndInitializePoolIfNecessary", + "outputs": [ + { + "internalType": "address", + "name": "pool", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "amount0Min", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Min", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", + "name": "params", + "type": "tuple" + } + ], + "name": "decreaseLiquidity", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount0Desired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Desired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount0Min", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Min", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct INonfungiblePositionManager.IncreaseLiquidityParams", + "name": "params", + "type": "tuple" + } + ], + "name": "increaseLiquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "uint256", + "name": "amount0Desired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Desired", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount0Min", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Min", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct INonfungiblePositionManager.MintParams", + "name": "params", + "type": "tuple" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "data", + "type": "bytes[]" + } + ], + "name": "multicall", + "outputs": [ + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "positions", + "outputs": [ + { + "internalType": "uint96", + "name": "nonce", + "type": "uint96" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "address", + "name": "token0", + "type": "address" + }, + { + "internalType": "address", + "name": "token1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside0LastX128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside1LastX128", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "tokensOwed0", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "tokensOwed1", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "refundETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "_data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "selfPermit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "selfPermitAllowed", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "selfPermitAllowedIfNecessary", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "selfPermitIfNecessary", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amountMinimum", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "sweepToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "tokenOfOwnerByIndex", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount0Owed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1Owed", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "uniswapV3MintCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amountMinimum", + "type": "uint256" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "unwrapWETH9", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/uniswap/assets/uniswap-v3/pool.abi b/uniswap/assets/uniswap-v3/pool.abi new file mode 100644 index 00000000..49cc338e --- /dev/null +++ b/uniswap/assets/uniswap-v3/pool.abi @@ -0,0 +1,988 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount0", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount1", + "type": "uint128" + } + ], + "name": "Collect", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount0", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount1", + "type": "uint128" + } + ], + "name": "CollectProtocol", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "paid0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "paid1", + "type": "uint256" + } + ], + "name": "Flash", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "observationCardinalityNextOld", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "observationCardinalityNextNew", + "type": "uint16" + } + ], + "name": "IncreaseObservationCardinalityNext", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "Initialize", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "feeProtocol0Old", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "feeProtocol1Old", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "feeProtocol0New", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "feeProtocol1New", + "type": "uint8" + } + ], + "name": "SetFeeProtocol", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "amount0", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "amount1", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "Swap", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "name": "burn", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "amount0Requested", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1Requested", + "type": "uint128" + } + ], + "name": "collect", + "outputs": [ + { + "internalType": "uint128", + "name": "amount0", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1", + "type": "uint128" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "amount0Requested", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1Requested", + "type": "uint128" + } + ], + "name": "collectProtocol", + "outputs": [ + { + "internalType": "uint128", + "name": "amount0", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1", + "type": "uint128" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "fee", + "outputs": [ + { + "internalType": "uint24", + "name": "", + "type": "uint24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeGrowthGlobal0X128", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeGrowthGlobal1X128", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "flash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "observationCardinalityNext", + "type": "uint16" + } + ], + "name": "increaseObservationCardinalityNext", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "liquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxLiquidityPerTick", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "observations", + "outputs": [ + { + "internalType": "uint32", + "name": "blockTimestamp", + "type": "uint32" + }, + { + "internalType": "int56", + "name": "tickCumulative", + "type": "int56" + }, + { + "internalType": "uint160", + "name": "secondsPerLiquidityCumulativeX128", + "type": "uint160" + }, + { + "internalType": "bool", + "name": "initialized", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32[]", + "name": "secondsAgos", + "type": "uint32[]" + } + ], + "name": "observe", + "outputs": [ + { + "internalType": "int56[]", + "name": "tickCumulatives", + "type": "int56[]" + }, + { + "internalType": "uint160[]", + "name": "secondsPerLiquidityCumulativeX128s", + "type": "uint160[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "positions", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside0LastX128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside1LastX128", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "tokensOwed0", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "tokensOwed1", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolFees", + "outputs": [ + { + "internalType": "uint128", + "name": "token0", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "token1", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "feeProtocol0", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeProtocol1", + "type": "uint8" + } + ], + "name": "setFeeProtocol", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "slot0", + "outputs": [ + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "uint16", + "name": "observationIndex", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "observationCardinality", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "observationCardinalityNext", + "type": "uint16" + }, + { + "internalType": "uint8", + "name": "feeProtocol", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "unlocked", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + } + ], + "name": "snapshotCumulativesInside", + "outputs": [ + { + "internalType": "int56", + "name": "tickCumulativeInside", + "type": "int56" + }, + { + "internalType": "uint160", + "name": "secondsPerLiquidityInsideX128", + "type": "uint160" + }, + { + "internalType": "uint32", + "name": "secondsInside", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "int256", + "name": "amountSpecified", + "type": "int256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "int256", + "name": "amount0", + "type": "int256" + }, + { + "internalType": "int256", + "name": "amount1", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int16", + "name": "", + "type": "int16" + } + ], + "name": "tickBitmap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tickSpacing", + "outputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "name": "ticks", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidityGross", + "type": "uint128" + }, + { + "internalType": "int128", + "name": "liquidityNet", + "type": "int128" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside0X128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside1X128", + "type": "uint256" + }, + { + "internalType": "int56", + "name": "tickCumulativeOutside", + "type": "int56" + }, + { + "internalType": "uint160", + "name": "secondsPerLiquidityOutsideX128", + "type": "uint160" + }, + { + "internalType": "uint32", + "name": "secondsOutside", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "initialized", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token0", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token1", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/uniswap/assets/uniswap-v3/quoter.abi b/uniswap/assets/uniswap-v3/quoter.abi new file mode 100644 index 00000000..e9adacbe --- /dev/null +++ b/uniswap/assets/uniswap-v3/quoter.abi @@ -0,0 +1,97 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "_factory", "type": "address" }, + { "internalType": "address", "name": "_WETH9", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "WETH9", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes", "name": "path", "type": "bytes" }, + { "internalType": "uint256", "name": "amountIn", "type": "uint256" } + ], + "name": "quoteExactInput", + "outputs": [ + { "internalType": "uint256", "name": "amountOut", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "tokenIn", "type": "address" }, + { "internalType": "address", "name": "tokenOut", "type": "address" }, + { "internalType": "uint24", "name": "fee", "type": "uint24" }, + { "internalType": "uint256", "name": "amountIn", "type": "uint256" }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "name": "quoteExactInputSingle", + "outputs": [ + { "internalType": "uint256", "name": "amountOut", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes", "name": "path", "type": "bytes" }, + { "internalType": "uint256", "name": "amountOut", "type": "uint256" } + ], + "name": "quoteExactOutput", + "outputs": [ + { "internalType": "uint256", "name": "amountIn", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "tokenIn", "type": "address" }, + { "internalType": "address", "name": "tokenOut", "type": "address" }, + { "internalType": "uint24", "name": "fee", "type": "uint24" }, + { "internalType": "uint256", "name": "amountOut", "type": "uint256" }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "name": "quoteExactOutputSingle", + "outputs": [ + { "internalType": "uint256", "name": "amountIn", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "int256", "name": "amount0Delta", "type": "int256" }, + { "internalType": "int256", "name": "amount1Delta", "type": "int256" }, + { "internalType": "bytes", "name": "path", "type": "bytes" } + ], + "name": "uniswapV3SwapCallback", + "outputs": [], + "stateMutability": "view", + "type": "function" + } +] diff --git a/uniswap/assets/uniswap-v3/router.abi b/uniswap/assets/uniswap-v3/router.abi new file mode 100644 index 00000000..15f915b6 --- /dev/null +++ b/uniswap/assets/uniswap-v3/router.abi @@ -0,0 +1,274 @@ +[ + { + "inputs": [ + { "internalType": "address", "name": "_factory", "type": "address" }, + { "internalType": "address", "name": "_WETH9", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "WETH9", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes", "name": "path", "type": "bytes" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint256", "name": "amountIn", "type": "uint256" }, + { + "internalType": "uint256", + "name": "amountOutMinimum", + "type": "uint256" + } + ], + "internalType": "struct ISwapRouter.ExactInputParams", + "name": "params", + "type": "tuple" + } + ], + "name": "exactInput", + "outputs": [ + { "internalType": "uint256", "name": "amountOut", "type": "uint256" } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "tokenIn", "type": "address" }, + { "internalType": "address", "name": "tokenOut", "type": "address" }, + { "internalType": "uint24", "name": "fee", "type": "uint24" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint256", "name": "amountIn", "type": "uint256" }, + { + "internalType": "uint256", + "name": "amountOutMinimum", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "internalType": "struct ISwapRouter.ExactInputSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "exactInputSingle", + "outputs": [ + { "internalType": "uint256", "name": "amountOut", "type": "uint256" } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "bytes", "name": "path", "type": "bytes" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint256", "name": "amountOut", "type": "uint256" }, + { + "internalType": "uint256", + "name": "amountInMaximum", + "type": "uint256" + } + ], + "internalType": "struct ISwapRouter.ExactOutputParams", + "name": "params", + "type": "tuple" + } + ], + "name": "exactOutput", + "outputs": [ + { "internalType": "uint256", "name": "amountIn", "type": "uint256" } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "address", "name": "tokenIn", "type": "address" }, + { "internalType": "address", "name": "tokenOut", "type": "address" }, + { "internalType": "uint24", "name": "fee", "type": "uint24" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint256", "name": "amountOut", "type": "uint256" }, + { + "internalType": "uint256", + "name": "amountInMaximum", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "internalType": "struct ISwapRouter.ExactOutputSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "exactOutputSingle", + "outputs": [ + { "internalType": "uint256", "name": "amountIn", "type": "uint256" } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes[]", "name": "data", "type": "bytes[]" } + ], + "name": "multicall", + "outputs": [ + { "internalType": "bytes[]", "name": "results", "type": "bytes[]" } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "refundETH", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "selfPermit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "expiry", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "selfPermitAllowed", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "uint256", "name": "expiry", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "selfPermitAllowedIfNecessary", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "selfPermitIfNecessary", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amountMinimum", "type": "uint256" }, + { "internalType": "address", "name": "recipient", "type": "address" } + ], + "name": "sweepToken", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "uint256", "name": "amountMinimum", "type": "uint256" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "feeBips", "type": "uint256" }, + { "internalType": "address", "name": "feeRecipient", "type": "address" } + ], + "name": "sweepTokenWithFee", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "int256", "name": "amount0Delta", "type": "int256" }, + { "internalType": "int256", "name": "amount1Delta", "type": "int256" }, + { "internalType": "bytes", "name": "_data", "type": "bytes" } + ], + "name": "uniswapV3SwapCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "amountMinimum", "type": "uint256" }, + { "internalType": "address", "name": "recipient", "type": "address" } + ], + "name": "unwrapWETH9", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "amountMinimum", "type": "uint256" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "feeBips", "type": "uint256" }, + { "internalType": "address", "name": "feeRecipient", "type": "address" } + ], + "name": "unwrapWETH9WithFee", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { "stateMutability": "payable", "type": "receive" } +] diff --git a/uniswap/assets/uniswap-v4/permit2.abi b/uniswap/assets/uniswap-v4/permit2.abi new file mode 100644 index 00000000..2f0281eb --- /dev/null +++ b/uniswap/assets/uniswap-v4/permit2.abi @@ -0,0 +1,901 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "AllowanceExpired", + "type": "error" + }, + { + "inputs": [], + "name": "ExcessiveInvalidation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "maxAmount", + "type": "uint256" + } + ], + "name": "InvalidAmount", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidContractSignature", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidNonce", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSignature", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSignatureLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSigner", + "type": "error" + }, + { + "inputs": [], + "name": "LengthMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "signatureDeadline", + "type": "uint256" + } + ], + "name": "SignatureExpired", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "Lockdown", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "newNonce", + "type": "uint48" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "oldNonce", + "type": "uint48" + } + ], + "name": "NonceInvalidation", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "nonce", + "type": "uint48" + } + ], + "name": "Permit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "word", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "mask", + "type": "uint256" + } + ], + "name": "UnorderedNonceInvalidation", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { + "internalType": "uint48", + "name": "nonce", + "type": "uint48" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint48", + "name": "newNonce", + "type": "uint48" + } + ], + "name": "invalidateNonces", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "wordPos", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "mask", + "type": "uint256" + } + ], + "name": "invalidateUnorderedNonces", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "internalType": "struct IAllowanceTransfer.TokenSpenderPair[]", + "name": "approvals", + "type": "tuple[]" + } + ], + "name": "lockdown", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "nonceBitmap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { + "internalType": "uint48", + "name": "nonce", + "type": "uint48" + } + ], + "internalType": "struct IAllowanceTransfer.PermitDetails[]", + "name": "details", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sigDeadline", + "type": "uint256" + } + ], + "internalType": "struct IAllowanceTransfer.PermitBatch", + "name": "permitBatch", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { + "internalType": "uint48", + "name": "nonce", + "type": "uint48" + } + ], + "internalType": "struct IAllowanceTransfer.PermitDetails", + "name": "details", + "type": "tuple" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sigDeadline", + "type": "uint256" + } + ], + "internalType": "struct IAllowanceTransfer.PermitSingle", + "name": "permitSingle", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions", + "name": "permitted", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.PermitTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "requestedAmount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails", + "name": "transferDetails", + "type": "tuple" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "permitTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions[]", + "name": "permitted", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "requestedAmount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails[]", + "name": "transferDetails", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "permitTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions", + "name": "permitted", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.PermitTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "requestedAmount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails", + "name": "transferDetails", + "type": "tuple" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "witness", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "witnessTypeString", + "type": "string" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "permitWitnessTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.TokenPermissions[]", + "name": "permitted", + "type": "tuple[]" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.PermitBatchTransferFrom", + "name": "permit", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "requestedAmount", + "type": "uint256" + } + ], + "internalType": "struct ISignatureTransfer.SignatureTransferDetails[]", + "name": "transferDetails", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "witness", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "witnessTypeString", + "type": "string" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "permitWitnessTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "internalType": "struct IAllowanceTransfer.AllowanceTransferDetails[]", + "name": "transferDetails", + "type": "tuple[]" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/uniswap/assets/uniswap-v4/poolmanager.abi b/uniswap/assets/uniswap-v4/poolmanager.abi new file mode 100644 index 00000000..a4ef1f84 --- /dev/null +++ b/uniswap/assets/uniswap-v4/poolmanager.abi @@ -0,0 +1,1366 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "initialOwner", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AlreadyUnlocked", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "currency0", + "type": "address" + }, + { + "internalType": "address", + "name": "currency1", + "type": "address" + } + ], + "name": "CurrenciesOutOfOrderOrEqual", + "type": "error" + }, + { + "inputs": [], + "name": "CurrencyNotSettled", + "type": "error" + }, + { + "inputs": [], + "name": "DelegateCallNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCaller", + "type": "error" + }, + { + "inputs": [], + "name": "ManagerLocked", + "type": "error" + }, + { + "inputs": [], + "name": "MustClearExactPositiveDelta", + "type": "error" + }, + { + "inputs": [], + "name": "NonzeroNativeValue", + "type": "error" + }, + { + "inputs": [], + "name": "PoolNotInitialized", + "type": "error" + }, + { + "inputs": [], + "name": "ProtocolFeeCurrencySynced", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + } + ], + "name": "ProtocolFeeTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "SwapAmountCannotBeZero", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + } + ], + "name": "TickSpacingTooLarge", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + } + ], + "name": "TickSpacingTooSmall", + "type": "error" + }, + { + "inputs": [], + "name": "UnauthorizedDynamicLPFeeUpdate", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "PoolId", + "name": "id", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "Donate", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "PoolId", + "name": "id", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "indexed": true, + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "indexed": false, + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "Initialize", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "PoolId", + "name": "id", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "int256", + "name": "liquidityDelta", + "type": "int256" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "name": "ModifyLiquidity", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "OperatorSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "protocolFeeController", + "type": "address" + } + ], + "name": "ProtocolFeeControllerUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "PoolId", + "name": "id", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint24", + "name": "protocolFee", + "type": "uint24" + } + ], + "name": "ProtocolFeeUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "PoolId", + "name": "id", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": false, + "internalType": "int128", + "name": "amount0", + "type": "int128" + }, + { + "indexed": false, + "internalType": "int128", + "name": "amount1", + "type": "int128" + }, + { + "indexed": false, + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint24", + "name": "fee", + "type": "uint24" + } + ], + "name": "Swap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "clear", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "collectProtocolFees", + "outputs": [ + { + "internalType": "uint256", + "name": "amountCollected", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "name": "donate", + "outputs": [ + { + "internalType": "BalanceDelta", + "name": "delta", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "extsload", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "startSlot", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "nSlots", + "type": "uint256" + } + ], + "name": "extsload", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "slots", + "type": "bytes32[]" + } + ], + "name": "extsload", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "slots", + "type": "bytes32[]" + } + ], + "name": "exttload", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "exttload", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + } + ], + "name": "initialize", + "outputs": [ + { + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isOperator", + "outputs": [ + { + "internalType": "bool", + "name": "isOperator", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "mint", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "int256", + "name": "liquidityDelta", + "type": "int256" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "internalType": "struct IPoolManager.ModifyLiquidityParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "name": "modifyLiquidity", + "outputs": [ + { + "internalType": "BalanceDelta", + "name": "callerDelta", + "type": "int256" + }, + { + "internalType": "BalanceDelta", + "name": "feesAccrued", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolFeeController", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + } + ], + "name": "protocolFeesAccrued", + "outputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setOperator", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "uint24", + "name": "newProtocolFee", + "type": "uint24" + } + ], + "name": "setProtocolFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "controller", + "type": "address" + } + ], + "name": "setProtocolFeeController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "settle", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + } + ], + "name": "settleFor", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "components": [ + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "int256", + "name": "amountSpecified", + "type": "int256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + } + ], + "internalType": "struct IPoolManager.SwapParams", + "name": "params", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "BalanceDelta", + "name": "swapDelta", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + } + ], + "name": "sync", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "take", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "unlock", + "outputs": [ + { + "internalType": "bytes", + "name": "result", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "uint24", + "name": "newDynamicLPFee", + "type": "uint24" + } + ], + "name": "updateDynamicLPFee", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/uniswap/assets/uniswap-v4/pos_descriptor.abi b/uniswap/assets/uniswap-v4/pos_descriptor.abi new file mode 100644 index 00000000..95a0e65d --- /dev/null +++ b/uniswap/assets/uniswap-v4/pos_descriptor.abi @@ -0,0 +1,167 @@ +[ + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "_poolManager", + "type": "address" + }, + { + "internalType": "address", + "name": "_wrappedNative", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_nativeCurrencyLabelBytes", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "len", + "type": "uint256" + } + ], + "name": "InvalidAddressLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "InvalidTokenId", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "StringsInsufficientHexLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "currency", + "type": "address" + } + ], + "name": "currencyRatioPriority", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "currency0", + "type": "address" + }, + { + "internalType": "address", + "name": "currency1", + "type": "address" + } + ], + "name": "flipRatio", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nativeCurrencyLabel", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolManager", + "outputs": [ + { + "internalType": "contract IPoolManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPositionManager", + "name": "positionManager", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "wrappedNative", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/uniswap/assets/uniswap-v4/pos_manager.abi b/uniswap/assets/uniswap-v4/pos_manager.abi new file mode 100644 index 00000000..7fd6f0a6 --- /dev/null +++ b/uniswap/assets/uniswap-v4/pos_manager.abi @@ -0,0 +1,1297 @@ +[ + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "_poolManager", + "type": "address" + }, + { + "internalType": "contract IAllowanceTransfer", + "name": "_permit2", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_unsubscribeGasLimit", + "type": "uint256" + }, + { + "internalType": "contract IPositionDescriptor", + "name": "_tokenDescriptor", + "type": "address" + }, + { + "internalType": "contract IWETH9", + "name": "_weth9", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "subscriber", + "type": "address" + } + ], + "name": "AlreadySubscribed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "subscriber", + "type": "address" + }, + { + "internalType": "bytes", + "name": "reason", + "type": "bytes" + } + ], + "name": "BurnNotificationReverted", + "type": "error" + }, + { + "inputs": [], + "name": "ContractLocked", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "DeadlinePassed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + } + ], + "name": "DeltaNotNegative", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + } + ], + "name": "DeltaNotPositive", + "type": "error" + }, + { + "inputs": [], + "name": "GasLimitTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "InputLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidContractSignature", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidEthSender", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSignature", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSignatureLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidSigner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "maximumAmount", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amountRequested", + "type": "uint128" + } + ], + "name": "MaximumAmountExceeded", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint128", + "name": "minimumAmount", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amountReceived", + "type": "uint128" + } + ], + "name": "MinimumAmountInsufficient", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "subscriber", + "type": "address" + }, + { + "internalType": "bytes", + "name": "reason", + "type": "bytes" + } + ], + "name": "ModifyLiquidityNotificationReverted", + "type": "error" + }, + { + "inputs": [], + "name": "NoCodeSubscriber", + "type": "error" + }, + { + "inputs": [], + "name": "NoSelfPermit", + "type": "error" + }, + { + "inputs": [], + "name": "NonceAlreadyUsed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + } + ], + "name": "NotApproved", + "type": "error" + }, + { + "inputs": [], + "name": "NotPoolManager", + "type": "error" + }, + { + "inputs": [], + "name": "NotSubscribed", + "type": "error" + }, + { + "inputs": [], + "name": "PoolManagerMustBeLocked", + "type": "error" + }, + { + "inputs": [], + "name": "SignatureDeadlineExpired", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "subscriber", + "type": "address" + }, + { + "internalType": "bytes", + "name": "reason", + "type": "bytes" + } + ], + "name": "SubscriptionReverted", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "action", + "type": "uint256" + } + ], + "name": "UnsupportedAction", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "subscriber", + "type": "address" + } + ], + "name": "Subscription", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "subscriber", + "type": "address" + } + ], + "name": "Unsubscription", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "WETH9", + "outputs": [ + { + "internalType": "contract IWETH9", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "getApproved", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getPoolAndPositionInfo", + "outputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "poolKey", + "type": "tuple" + }, + { + "internalType": "PositionInfo", + "name": "info", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "getPositionLiquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + } + ], + "name": "initializePool", + "outputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + }, + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "unlockData", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "modifyLiquidities", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "actions", + "type": "bytes" + }, + { + "internalType": "bytes[]", + "name": "params", + "type": "bytes[]" + } + ], + "name": "modifyLiquiditiesWithoutUnlock", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "msgSender", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "data", + "type": "bytes[]" + } + ], + "name": "multicall", + "outputs": [ + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "nextTokenId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint256", + "name": "word", + "type": "uint256" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "bitmap", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "ownerOf", + "outputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { + "internalType": "uint48", + "name": "nonce", + "type": "uint48" + } + ], + "internalType": "struct IAllowanceTransfer.PermitDetails", + "name": "details", + "type": "tuple" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sigDeadline", + "type": "uint256" + } + ], + "internalType": "struct IAllowanceTransfer.PermitSingle", + "name": "permitSingle", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "permit", + "outputs": [ + { + "internalType": "bytes", + "name": "err", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "permit2", + "outputs": [ + { + "internalType": "contract IAllowanceTransfer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "components": [ + { + "components": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint160", + "name": "amount", + "type": "uint160" + }, + { + "internalType": "uint48", + "name": "expiration", + "type": "uint48" + }, + { + "internalType": "uint48", + "name": "nonce", + "type": "uint48" + } + ], + "internalType": "struct IAllowanceTransfer.PermitDetails[]", + "name": "details", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sigDeadline", + "type": "uint256" + } + ], + "internalType": "struct IAllowanceTransfer.PermitBatch", + "name": "_permitBatch", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "permitBatch", + "outputs": [ + { + "internalType": "bytes", + "name": "err", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "permitForAll", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes25", + "name": "poolId", + "type": "bytes25" + } + ], + "name": "poolKeys", + "outputs": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolManager", + "outputs": [ + { + "internalType": "contract IPoolManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "positionInfo", + "outputs": [ + { + "internalType": "PositionInfo", + "name": "info", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + } + ], + "name": "revokeNonce", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "newSubscriber", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "subscribe", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "subscriber", + "outputs": [ + { + "internalType": "contract ISubscriber", + "name": "subscriber", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tokenDescriptor", + "outputs": [ + { + "internalType": "contract IPositionDescriptor", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "tokenURI", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "unlockCallback", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "unsubscribe", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "unsubscribeGasLimit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] \ No newline at end of file diff --git a/uniswap/assets/uniswap-v4/quoter.abi b/uniswap/assets/uniswap-v4/quoter.abi new file mode 100644 index 00000000..f38100bc --- /dev/null +++ b/uniswap/assets/uniswap-v4/quoter.abi @@ -0,0 +1,637 @@ +[ + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "_poolManager", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + } + ], + "name": "NotEnoughLiquidity", + "type": "error" + }, + { + "inputs": [], + "name": "NotPoolManager", + "type": "error" + }, + { + "inputs": [], + "name": "NotSelf", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "QuoteSwap", + "type": "error" + }, + { + "inputs": [], + "name": "UnexpectedCallSuccess", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "revertData", + "type": "bytes" + } + ], + "name": "UnexpectedRevertBytes", + "type": "error" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "exactCurrency", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "intermediateCurrency", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct PathKey[]", + "name": "path", + "type": "tuple[]" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + } + ], + "internalType": "struct IV4Quoter.QuoteExactParams", + "name": "params", + "type": "tuple" + } + ], + "name": "_quoteExactInput", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "poolKey", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct IV4Quoter.QuoteExactSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "_quoteExactInputSingle", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "exactCurrency", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "intermediateCurrency", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct PathKey[]", + "name": "path", + "type": "tuple[]" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + } + ], + "internalType": "struct IV4Quoter.QuoteExactParams", + "name": "params", + "type": "tuple" + } + ], + "name": "_quoteExactOutput", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "poolKey", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct IV4Quoter.QuoteExactSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "_quoteExactOutputSingle", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "poolManager", + "outputs": [ + { + "internalType": "contract IPoolManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "exactCurrency", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "intermediateCurrency", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct PathKey[]", + "name": "path", + "type": "tuple[]" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + } + ], + "internalType": "struct IV4Quoter.QuoteExactParams", + "name": "params", + "type": "tuple" + } + ], + "name": "quoteExactInput", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasEstimate", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "poolKey", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct IV4Quoter.QuoteExactSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "quoteExactInputSingle", + "outputs": [ + { + "internalType": "uint256", + "name": "amountOut", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasEstimate", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "Currency", + "name": "exactCurrency", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "intermediateCurrency", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct PathKey[]", + "name": "path", + "type": "tuple[]" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + } + ], + "internalType": "struct IV4Quoter.QuoteExactParams", + "name": "params", + "type": "tuple" + } + ], + "name": "quoteExactOutput", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasEstimate", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "poolKey", + "type": "tuple" + }, + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "uint128", + "name": "exactAmount", + "type": "uint128" + }, + { + "internalType": "bytes", + "name": "hookData", + "type": "bytes" + } + ], + "internalType": "struct IV4Quoter.QuoteExactSingleParams", + "name": "params", + "type": "tuple" + } + ], + "name": "quoteExactOutputSingle", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasEstimate", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "unlockCallback", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/uniswap/assets/uniswap-v4/reserves_lens.abi b/uniswap/assets/uniswap-v4/reserves_lens.abi new file mode 100644 index 00000000..569e6ee5 --- /dev/null +++ b/uniswap/assets/uniswap-v4/reserves_lens.abi @@ -0,0 +1,996 @@ +[ + { + "inputs": [ + { + "internalType": "uint256", + "name": "expected", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actual", + "type": "uint256" + } + ], + "name": "CursorBlockMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "CursorContextMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "CursorStateMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "InputLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCursor", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint32", + "name": "maxReads", + "type": "uint32" + } + ], + "name": "InvalidScanBudget", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + } + ], + "name": "InvalidTickSpacing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + } + ], + "name": "LiquidityInvariantFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "manager", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "firstSlot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "lastSlot", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "count", + "type": "uint256" + } + ], + "name": "ManagerBatchReadFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + } + ], + "name": "PoolNotInitialized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int256", + "name": "tick", + "type": "int256" + } + ], + "name": "TickInvariantFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "UnsupportedCursorVersion", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "manager", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "address", + "name": "statsProvider", + "type": "address" + } + ], + "name": "getPoolTVL", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "coreAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "coreAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective1", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "activeLiquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "address", + "name": "statsProvider", + "type": "address" + }, + { + "internalType": "uint16", + "name": "hookPermissions", + "type": "uint16" + }, + { + "internalType": "bool", + "name": "hasCustomAccounting", + "type": "bool" + }, + { + "internalType": "enum IReservesLens.HookStatsStatus", + "name": "statsStatus", + "type": "uint8" + } + ], + "internalType": "struct IReservesLens.PoolTVL", + "name": "result", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "manager", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + } + ], + "name": "getPoolTVL", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "coreAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "coreAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective1", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "activeLiquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "address", + "name": "statsProvider", + "type": "address" + }, + { + "internalType": "uint16", + "name": "hookPermissions", + "type": "uint16" + }, + { + "internalType": "bool", + "name": "hasCustomAccounting", + "type": "bool" + }, + { + "internalType": "enum IReservesLens.HookStatsStatus", + "name": "statsStatus", + "type": "uint8" + } + ], + "internalType": "struct IReservesLens.PoolTVL", + "name": "result", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "manager", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey[]", + "name": "keys", + "type": "tuple[]" + } + ], + "name": "getPoolTVLBatch", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "coreAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "coreAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective1", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "activeLiquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "address", + "name": "statsProvider", + "type": "address" + }, + { + "internalType": "uint16", + "name": "hookPermissions", + "type": "uint16" + }, + { + "internalType": "bool", + "name": "hasCustomAccounting", + "type": "bool" + }, + { + "internalType": "enum IReservesLens.HookStatsStatus", + "name": "statsStatus", + "type": "uint8" + } + ], + "internalType": "struct IReservesLens.PoolTVL[]", + "name": "results", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "manager", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey[]", + "name": "keys", + "type": "tuple[]" + }, + { + "internalType": "address[]", + "name": "statsProviders", + "type": "address[]" + } + ], + "name": "getPoolTVLBatch", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "coreAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "coreAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective1", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "activeLiquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "address", + "name": "statsProvider", + "type": "address" + }, + { + "internalType": "uint16", + "name": "hookPermissions", + "type": "uint16" + }, + { + "internalType": "bool", + "name": "hasCustomAccounting", + "type": "bool" + }, + { + "internalType": "enum IReservesLens.HookStatsStatus", + "name": "statsStatus", + "type": "uint8" + } + ], + "internalType": "struct IReservesLens.PoolTVL[]", + "name": "results", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "manager", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "address", + "name": "statsProvider", + "type": "address" + }, + { + "internalType": "bytes", + "name": "cursor", + "type": "bytes" + }, + { + "internalType": "uint32", + "name": "maxReads", + "type": "uint32" + } + ], + "name": "getPoolTVLPaged", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "coreAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "coreAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective1", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "activeLiquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "address", + "name": "statsProvider", + "type": "address" + }, + { + "internalType": "uint16", + "name": "hookPermissions", + "type": "uint16" + }, + { + "internalType": "bool", + "name": "hasCustomAccounting", + "type": "bool" + }, + { + "internalType": "enum IReservesLens.HookStatsStatus", + "name": "statsStatus", + "type": "uint8" + } + ], + "internalType": "struct IReservesLens.PoolTVL", + "name": "result", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "nextCursor", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "done", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "manager", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "cursor", + "type": "bytes" + } + ], + "name": "getPoolTVLPaged", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "coreAmount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "coreAmount1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookReserves1", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "hookEffective1", + "type": "uint256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "activeLiquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "blockNumber", + "type": "uint256" + }, + { + "internalType": "address", + "name": "statsProvider", + "type": "address" + }, + { + "internalType": "uint16", + "name": "hookPermissions", + "type": "uint16" + }, + { + "internalType": "bool", + "name": "hasCustomAccounting", + "type": "bool" + }, + { + "internalType": "enum IReservesLens.HookStatsStatus", + "name": "statsStatus", + "type": "uint8" + } + ], + "internalType": "struct IReservesLens.PoolTVL", + "name": "result", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "nextCursor", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "done", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "manager", + "type": "address" + }, + { + "components": [ + { + "internalType": "Currency", + "name": "currency0", + "type": "address" + }, + { + "internalType": "Currency", + "name": "currency1", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "int24", + "name": "tickSpacing", + "type": "int24" + }, + { + "internalType": "contract IHooks", + "name": "hooks", + "type": "address" + } + ], + "internalType": "struct PoolKey", + "name": "key", + "type": "tuple" + }, + { + "internalType": "int16", + "name": "wordPos", + "type": "int16" + } + ], + "name": "getPopulatedTicksInWord", + "outputs": [ + { + "components": [ + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "int128", + "name": "liquidityNet", + "type": "int128" + }, + { + "internalType": "uint128", + "name": "liquidityGross", + "type": "uint128" + } + ], + "internalType": "struct IReservesLens.PopulatedTick[]", + "name": "populatedTicks", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + } +] diff --git a/uniswap/assets/uniswap-v4/router.abi b/uniswap/assets/uniswap-v4/router.abi new file mode 100644 index 00000000..7cca4f48 --- /dev/null +++ b/uniswap/assets/uniswap-v4/router.abi @@ -0,0 +1,848 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "permit2", + "type": "address" + }, + { + "internalType": "address", + "name": "weth9", + "type": "address" + }, + { + "internalType": "address", + "name": "v2Factory", + "type": "address" + }, + { + "internalType": "address", + "name": "v3Factory", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "pairInitCodeHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "poolInitCodeHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "v4PoolManager", + "type": "address" + }, + { + "internalType": "address", + "name": "v3NFTPositionManager", + "type": "address" + }, + { + "internalType": "address", + "name": "v4PositionManager", + "type": "address" + }, + { + "internalType": "address", + "name": "spokePool", + "type": "address" + } + ], + "internalType": "struct RouterParameters", + "name": "params", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "AddressInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "BalanceTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "ContractLocked", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + } + ], + "name": "DeltaNotNegative", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "Currency", + "name": "currency", + "type": "address" + } + ], + "name": "DeltaNotPositive", + "type": "error" + }, + { + "inputs": [], + "name": "ECDSAInvalidSignature", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "ECDSAInvalidSignatureLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "ECDSAInvalidSignatureS", + "type": "error" + }, + { + "inputs": [], + "name": "ETHNotAccepted", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "commandIndex", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "message", + "type": "bytes" + } + ], + "name": "ExecutionFailed", + "type": "error" + }, + { + "inputs": [], + "name": "FailedInnerCall", + "type": "error" + }, + { + "inputs": [], + "name": "FromAddressIsNotOwner", + "type": "error" + }, + { + "inputs": [], + "name": "InputLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientETH", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientToken", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "action", + "type": "bytes4" + } + ], + "name": "InvalidAction", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidBips", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "commandType", + "type": "uint256" + } + ], + "name": "InvalidCommandType", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidEthSender", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidHopPriceLength", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPath", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPortion", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidReserves", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidShortString", + "type": "error" + }, + { + "inputs": [], + "name": "LengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "NonceAlreadyUsed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "NotAuthorizedForToken", + "type": "error" + }, + { + "inputs": [], + "name": "NotPoolManager", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyMintAllowed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "SafeERC20FailedOperation", + "type": "error" + }, + { + "inputs": [], + "name": "SliceOutOfBounds", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "str", + "type": "string" + } + ], + "name": "StringTooLong", + "type": "error" + }, + { + "inputs": [], + "name": "TransactionDeadlinePassed", + "type": "error" + }, + { + "inputs": [], + "name": "UnsafeCast", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "action", + "type": "uint256" + } + ], + "name": "UnsupportedAction", + "type": "error" + }, + { + "inputs": [], + "name": "V2InvalidHopPriceLength", + "type": "error" + }, + { + "inputs": [], + "name": "V2InvalidPath", + "type": "error" + }, + { + "inputs": [], + "name": "V2TooLittleReceived", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "hopIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "name": "V2TooLittleReceivedPerHop", + "type": "error" + }, + { + "inputs": [], + "name": "V2TooMuchRequested", + "type": "error" + }, + { + "inputs": [], + "name": "V3HopPriceAndPathLengthMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "V3InvalidAmountOut", + "type": "error" + }, + { + "inputs": [], + "name": "V3InvalidCaller", + "type": "error" + }, + { + "inputs": [], + "name": "V3InvalidSwap", + "type": "error" + }, + { + "inputs": [], + "name": "V3TooLittleReceived", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "hopIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "name": "V3TooLittleReceivedPerHop", + "type": "error" + }, + { + "inputs": [], + "name": "V3TooMuchRequested", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "hopIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "name": "V3TooMuchRequestedPerHop", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "minAmountOutReceived", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountReceived", + "type": "uint256" + } + ], + "name": "V4TooLittleReceived", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "hopIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "name": "V4TooLittleReceivedPerHop", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "minPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "name": "V4TooLittleReceivedPerHopSingle", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "maxAmountInRequested", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amountRequested", + "type": "uint256" + } + ], + "name": "V4TooMuchRequested", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "hopIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "name": "V4TooMuchRequestedPerHop", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "minPrice", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "name": "V4TooMuchRequestedPerHopSingle", + "type": "error" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "inputs": [], + "name": "SPOKE_POOL", + "outputs": [ + { + "internalType": "contract IV3SpokePool", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "V3_POSITION_MANAGER", + "outputs": [ + { + "internalType": "contract INonfungiblePositionManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "V4_POSITION_MANAGER", + "outputs": [ + { + "internalType": "contract IPositionManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "commands", + "type": "bytes" + }, + { + "internalType": "bytes[]", + "name": "inputs", + "type": "bytes[]" + } + ], + "name": "execute", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "commands", + "type": "bytes" + }, + { + "internalType": "bytes[]", + "name": "inputs", + "type": "bytes[]" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "execute", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "commands", + "type": "bytes" + }, + { + "internalType": "bytes[]", + "name": "inputs", + "type": "bytes[]" + }, + { + "internalType": "bytes32", + "name": "intent", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "data", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "verifySender", + "type": "bool" + }, + { + "internalType": "bytes32", + "name": "nonce", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "executeSigned", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "msgSender", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "nonce", + "type": "bytes32" + } + ], + "name": "noncesUsed", + "outputs": [ + { + "internalType": "bool", + "name": "used", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolManager", + "outputs": [ + { + "internalType": "contract IPoolManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "signedRouteContext", + "outputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "intent", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "data", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "amount0Delta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "amount1Delta", + "type": "int256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "uniswapV3SwapCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "unlockCallback", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/uniswap/assets/uniswap-v4/stateview.abi b/uniswap/assets/uniswap-v4/stateview.abi new file mode 100644 index 00000000..19d58662 --- /dev/null +++ b/uniswap/assets/uniswap-v4/stateview.abi @@ -0,0 +1,365 @@ +[ + { + "inputs": [ + { + "internalType": "contract IPoolManager", + "name": "_poolManager", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + } + ], + "name": "getFeeGrowthGlobals", + "outputs": [ + { + "internalType": "uint256", + "name": "feeGrowthGlobal0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthGlobal1", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + } + ], + "name": "getFeeGrowthInside", + "outputs": [ + { + "internalType": "uint256", + "name": "feeGrowthInside0X128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside1X128", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + } + ], + "name": "getLiquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "positionId", + "type": "bytes32" + } + ], + "name": "getPositionInfo", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside0LastX128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside1LastX128", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + } + ], + "name": "getPositionInfo", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside0LastX128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside1LastX128", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "positionId", + "type": "bytes32" + } + ], + "name": "getPositionLiquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + } + ], + "name": "getSlot0", + "outputs": [ + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "uint24", + "name": "protocolFee", + "type": "uint24" + }, + { + "internalType": "uint24", + "name": "lpFee", + "type": "uint24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int16", + "name": "tick", + "type": "int16" + } + ], + "name": "getTickBitmap", + "outputs": [ + { + "internalType": "uint256", + "name": "tickBitmap", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "getTickFeeGrowthOutside", + "outputs": [ + { + "internalType": "uint256", + "name": "feeGrowthOutside0X128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside1X128", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "getTickInfo", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidityGross", + "type": "uint128" + }, + { + "internalType": "int128", + "name": "liquidityNet", + "type": "int128" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside0X128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside1X128", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "PoolId", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "getTickLiquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidityGross", + "type": "uint128" + }, + { + "internalType": "int128", + "name": "liquidityNet", + "type": "int128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "poolManager", + "outputs": [ + { + "internalType": "contract IPoolManager", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/uniswap/asyncuniswap.py b/uniswap/asyncuniswap.py new file mode 100644 index 00000000..b1298d8e --- /dev/null +++ b/uniswap/asyncuniswap.py @@ -0,0 +1,2136 @@ +import asyncio +import logging +import os +import time +from collections import namedtuple +from collections.abc import Iterable, Sequence +from typing import ( + Any, +) + +from eth_typing import HexStr +from eth_typing.evm import Address, ChecksumAddress +from hexbytes import HexBytes +from web3 import AsyncWeb3, Web3 +from web3._utils.abi import map_abi_data +from web3._utils.normalizers import BASE_RETURN_NORMALIZERS +from web3.contract import AsyncContract +from web3.contract.async_contract import AsyncContractFunction +from web3.exceptions import BadFunctionCallOutput, ContractLogicError +from web3.types import ( + Nonce, + TxParams, + TxReceipt, + Wei, +) + +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 .exceptions import InsufficientBalance, InvalidToken +from .fee import validate_fee_tier +from .token import ERC20Token +from .types import AddressLike +from .util import ( + _addr_to_str, + _load_abi, + _str_to_addr, + _validate_address, + chunks, + encode_sqrt_ratioX96, + is_same_address, + nearest_tick, + realised_fee_percentage, +) + +logger = logging.getLogger(__name__) + + +class AsyncUniswap: + """ + Wrapper around Uniswap contracts. + """ + + address: AddressLike + version: int + + w3: AsyncWeb3 + netid: int + netname: str + + default_slippage: float + use_estimate_gas: bool + + def __init__( + self, + address: AddressLike | str | None, + private_key: str | None, + provider: str | None = None, + web3: AsyncWeb3 | None = None, + version: int = 1, + default_slippage: float = 0.01, + use_estimate_gas: bool = True, + # use_eip1559: bool = True, + factory_contract_addr: str | None = None, + router_contract_addr: str | None = None, + enable_caching: bool = False, + ) -> None: + """ + :param address: The public address of the ETH wallet to use. + :param private_key: The private key of the ETH wallet to use. + :param provider: Can be optionally set to a Web3 provider URI. If none set, will fall back to the PROVIDER environment variable, or web3 if set. + :param web3: Can be optionally set to a custom Web3 instance. + :param version: Which version of the Uniswap contracts to use. + :param default_slippage: Default slippage for a trade, as a float (0.01 is 1%). WARNING: slippage is untested. + :param factory_contract_addr: Can be optionally set to override the address of the factory contract. + :param router_contract_addr: Can be optionally set to override the address of the router contract (v2 only). + :param enable_caching: Optionally enables middleware caching RPC method calls. + """ + self.address = _str_to_addr( + address or "0x0000000000000000000000000000000000000000" + ) + self.private_key = ( + private_key + or "0x0000000000000000000000000000000000000000000000000000000000000000" + ) + + self.version = version + if self.version not in [1, 2, 3]: + raise ValueError( + f"Invalid version '{self.version}', only 1, 2 or 3 supported" + ) # pragma: no cover + + # TODO: Write tests for slippage + self.default_slippage = default_slippage + self.use_estimate_gas = use_estimate_gas + + if web3: + self.w3 = web3 + # Derive sync provider URL from the supplied async web3 so we don't + # require PROVIDER in the environment when a web3 instance is given. + if not provider: + _ep = getattr(self.w3.provider, "endpoint_uri", None) + provider = str(_ep) if _ep is not None else os.environ["PROVIDER"] + else: + if not provider: + provider = os.environ["PROVIDER"] + # Initialize web3. Extra provider for testing. + self.w3 = AsyncWeb3( + AsyncWeb3.AsyncHTTPProvider(provider, request_kwargs={"timeout": 60}) + ) + tmp_w3 = Web3(Web3.HTTPProvider(provider, request_kwargs={"timeout": 60})) + self.netid = int(tmp_w3.net.version) + if self.netid in _netid_to_name: + self.netname = _netid_to_name[self.netid] + else: + raise ValueError(f"Unknown netid: {self.netid}") # pragma: no cover + logger.info(f"Using {self.w3} ('{self.netname}', netid: {self.netid})") + + self.last_nonce: Nonce = tmp_w3.eth.get_transaction_count(self.address) + + # This code automatically approves you for trading on the exchange. + # max_approval is to allow the contract to exchange on your behalf. + # max_approval_check checks that current approval is above a reasonable number + # The program cannot check for max_approval each time because it decreases + # with each trade. + max_approval_hex = f"0x{64 * 'f'}" + self.max_approval_int = int(max_approval_hex, 16) + max_approval_check_hex = f"0x{15 * '0'}{49 * 'f'}" + self.max_approval_check_int = int(max_approval_check_hex, 16) + + if self.version == 1: + if factory_contract_addr is None: + factory_contract_addr = _factory_contract_addresses_v1[self.netname] + + self.factory_contract = _load_contract( + self.w3, + abi_name="uniswap-v1/factory", + address=_str_to_addr(factory_contract_addr), + ) + elif self.version == 2: + if router_contract_addr is None: + router_contract_addr = _router_contract_addresses_v2[self.netname] + self.router_address: AddressLike = _str_to_addr(router_contract_addr) + + if factory_contract_addr is None: + factory_contract_addr = _factory_contract_addresses_v2[self.netname] + self.factory_contract = _load_contract( + self.w3, + abi_name="uniswap-v2/factory", + address=_str_to_addr(factory_contract_addr), + ) + # Documented here: https://uniswap.org/docs/v2/smart-contracts/router02/ + self.router = _load_contract( + self.w3, + abi_name="uniswap-v2/router02", + address=self.router_address, + ) + elif self.version == 3: + # https://github.com/Uniswap/uniswap-v3-periphery/blob/main/deploys.md + factory_contract_address = _str_to_addr( + "0x1F98431c8aD98523631AE4a59f267346ea31F984" + ) + self.factory_contract = _load_contract( + self.w3, abi_name="uniswap-v3/factory", address=factory_contract_address + ) + quoter_addr = _str_to_addr("0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6") + self.router_address = _str_to_addr( + "0xE592427A0AEce92De3Edee1F18E0157C05861564" + ) + self.quoter = _load_contract( + self.w3, abi_name="uniswap-v3/quoter", address=quoter_addr + ) + self.router = _load_contract( + self.w3, abi_name="uniswap-v3/router", address=self.router_address + ) + self.positionManager_addr = _str_to_addr( + "0xC36442b4a4522E871399CD717aBDD847Ab11FE88" + ) + self.nonFungiblePositionManager = _load_contract( + self.w3, + abi_name="uniswap-v3/nonFungiblePositionManager", + address=self.positionManager_addr, + ) + if self.netname == "arbitrum": + multicall2_addr = _str_to_addr( + "0x50075F151ABC5B6B448b1272A0a1cFb5CFA25828" + ) + else: + multicall2_addr = _str_to_addr( + "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696" + ) + self.multicall2 = _load_contract( + self.w3, abi_name="uniswap-v3/multicall", address=multicall2_addr + ) + else: + raise ValueError( + f"Invalid version '{self.version}', only 1, 2 or 3 supported" + ) + + if hasattr(self, "factory_contract"): + logger.info(f"Using factory contract: {self.factory_contract}") + + # ------ Market -------------------------------------------------------------------- + + async def get_price_input( + self, + token0: AddressLike, # input token + token1: AddressLike, # output token + qty: int, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> int: + """Given `qty` amount of the input `token0`, returns the maximum output amount of output `token1`.""" + fee = validate_fee_tier(fee=fee, version=self.version) + + if is_same_address(token0, ETH_ADDRESS) and self.version == 2: + return await self._get_eth_token_input_price(token1, Wei(qty), fee) + elif is_same_address(token1, ETH_ADDRESS) and self.version == 2: + return await self._get_token_eth_input_price(token0, qty, fee) + else: + if is_same_address(token0, ETH_ADDRESS): + token0 = await self.get_weth_address() + if is_same_address(token1, ETH_ADDRESS): + token1 = await self.get_weth_address() + return await self._get_token_token_input_price( + token0, token1, qty, fee, route, fees + ) + + async def get_price_output( + self, + token0: AddressLike, + token1: AddressLike, + qty: int, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> int: + """Returns the minimum amount of `token0` required to buy `qty` amount of `token1`.""" + fee = validate_fee_tier(fee=fee, version=self.version) + + if is_same_address(token0, ETH_ADDRESS) and self.version == 2: + return await self._get_eth_token_output_price(token1, qty, fee) + elif is_same_address(token1, ETH_ADDRESS) and self.version == 2: + return await self._get_token_eth_output_price(token0, Wei(qty), fee) + else: + if is_same_address(token0, ETH_ADDRESS): + token0 = await self.get_weth_address() + if is_same_address(token1, ETH_ADDRESS): + token1 = await self.get_weth_address() + return await self._get_token_token_output_price( + token0, token1, qty, fee, route, fees + ) + + async def _get_eth_token_input_price( + self, + token: AddressLike, # output token + qty: Wei, + fee: int, + ) -> Wei: + """Public price (i.e. amount of output token received) for ETH to token trades with an exact input.""" + if self.version == 1: + ex = await self._exchange_contract(token) + price: Wei = await ex.functions.getEthToTokenInputPrice(qty).call() + elif self.version == 2: + price = ( + await self.router.functions.getAmountsOut( + qty, [await self.get_weth_address(), token] + ).call() + )[-1] + elif self.version == 3: + price = await self._get_token_token_input_price( + await self.get_weth_address(), token, qty, fee=fee + ) # type: ignore + else: + raise ValueError # pragma: no cover + return price + + async def _get_token_eth_input_price( + self, + token: AddressLike, # input token + qty: int, + fee: int, + ) -> int: + """Public price (i.e. amount of ETH received) for token to ETH trades with an exact input.""" + if self.version == 1: + ex = await self._exchange_contract(token) + price: int = await ex.functions.getTokenToEthInputPrice(qty).call() + elif self.version == 2: + price = ( + await self.router.functions.getAmountsOut( + qty, [token, await self.get_weth_address()] + ).call() + )[-1] + elif self.version == 3: + price = await self._get_token_token_input_price( + token, await self.get_weth_address(), qty, fee=fee + ) + else: + raise ValueError # pragma: no cover + return price + + async def _get_token_token_input_price( + self, + token0: AddressLike, # input token + token1: AddressLike, # output token + qty: int, + fee: int, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> int: + """ + Public price (i.e. amount of output token received) for token to token trades with an exact input. + + :param fee: (v3 only) The pool's fee in hundredths of a bip, i.e. 1e-6 (3000 is 0.3%) + """ + if route is None and self.version == 2: + # If one of the tokens are WETH, delegate to appropriate call. + # See: https://github.com/shanefontaine/uniswap-python/issues/22 + if is_same_address(token0, await self.get_weth_address()): + return int(await self._get_eth_token_input_price(token1, Wei(qty), fee)) + elif is_same_address(token1, await self.get_weth_address()): + return int(await self._get_token_eth_input_price(token0, qty, fee)) + + route = [token0, await self.get_weth_address(), token1] + logger.warning(f"No route specified, assuming route: {route}") + + if self.version == 2: + price: int = (await self.router.functions.getAmountsOut(qty, route).call())[ + -1 + ] + elif self.version == 3: + if route and fees: + # 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.quoteExactInput(route, qty).call() + encoded_v3_path = self.encode_v3_path(route, fees) + price = await self.quoter.functions.quoteExactInput( + encoded_v3_path, qty + ).call() + else: + # FIXME: How to calculate this properly? See https://docs.uniswap.org/reference/libraries/SqrtPriceMath + sqrtPriceLimitX96 = 0 + price = await self.quoter.functions.quoteExactInputSingle( + token0, token1, fee, qty, sqrtPriceLimitX96 + ).call() + else: + raise ValueError("function not supported for this version of Uniswap") + return price + + async def _get_eth_token_output_price( + self, + token: AddressLike, # output token + qty: int, + fee: int | None = 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 = await self._exchange_contract(token) + price: Wei = await ex.functions.getEthToTokenOutputPrice(qty).call() + elif self.version == 2: + route = [await self.get_weth_address(), token] + price = (await self.router.functions.getAmountsIn(qty, route).call())[0] + elif self.version == 3: + price = Wei( + await self._get_token_token_output_price( + await self.get_weth_address(), token, qty, fee=fee + ) + ) + else: + raise ValueError # pragma: no cover + return price + + async def _get_token_eth_output_price( + self, + token: AddressLike, + qty: Wei, + fee: int | None = 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 = await self._exchange_contract(token) + price: int = await ex.functions.getTokenToEthOutputPrice(qty).call() + elif self.version == 2: + route = [token, await self.get_weth_address()] + price = (await self.router.functions.getAmountsIn(qty, route).call())[0] + elif self.version == 3: + price = await self._get_token_token_output_price( + token, await self.get_weth_address(), qty, fee=fee + ) + else: + raise ValueError # pragma: no cover + return price + + async def _get_token_token_output_price( + self, + token0: AddressLike, # input token + token1: AddressLike, # output token + qty: int, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> int: + """ + Public price (i.e. amount of input token needed) for token to token trades with an exact output. + + :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 and self.version == 2: + # If one of the tokens are WETH, delegate to appropriate call. + # See: https://github.com/shanefontaine/uniswap-python/issues/22 + if is_same_address(token0, await self.get_weth_address()): + return int(await self._get_eth_token_output_price(token1, qty, fee)) + elif is_same_address(token1, await self.get_weth_address()): + return int( + await self._get_token_eth_output_price(token0, Wei(qty), fee) + ) + + route = [token0, await self.get_weth_address(), token1] + logger.warning(f"No route specified, assuming route: {route}") + + if self.version == 2: + price: int = (await self.router.functions.getAmountsIn(qty, route).call())[ + 0 + ] + elif self.version == 3: + if route and fees: + # 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() + encoded_v3_path = self.encode_v3_path(route, fees, is_exact_out=True) + price = await self.quoter.functions.quoteExactOutput( + encoded_v3_path, qty + ).call() + else: + # FIXME: How to calculate this properly? + # - https://docs.uniswap.org/reference/libraries/SqrtPriceMath + # - https://github.com/Uniswap/uniswap-v3-sdk/blob/main/src/swapRouter.ts + sqrtPriceLimitX96 = 0 + price = await self.quoter.functions.quoteExactOutputSingle( + token0, token1, fee, qty, sqrtPriceLimitX96 + ).call() + else: + raise ValueError # pragma: no cover + return price + + # ------ Make Trade ---------------------------------------------------------------- + async def make_trade( + self, + input_token: AddressLike, + output_token: AddressLike, + qty: int | Wei, + recipient: AddressLike | None = None, + fee: int | None = None, + slippage: float | None = None, + fee_on_transfer: bool = False, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> HexBytes: + """Make a trade by defining the qty of the input token.""" + if not isinstance(qty, int): + raise TypeError("swapped quantity must be an integer") + + fee = validate_fee_tier(fee=fee, version=self.version) + + if slippage is None: + slippage = self.default_slippage + + if input_token == output_token: + raise ValueError + + if self.version != 3 and fees: + fees = None + + if input_token == ETH_ADDRESS and (route is None or fees is None): + return await self._eth_to_token_swap_input( + output_token, Wei(qty), recipient, fee, slippage, fee_on_transfer + ) + elif output_token == ETH_ADDRESS and (route is None or fees is None): + return await self._token_to_eth_swap_input( + input_token, qty, recipient, fee, slippage, fee_on_transfer + ) + else: + # if is_same_address(input_token, ETH_ADDRESS): + # input_token = await self.get_weth_address() + # if is_same_address(output_token, ETH_ADDRESS): + # output_token = await self.get_weth_address() + return await self._token_to_token_swap_input( + input_token, + output_token, + qty, + recipient, + fee, + slippage, + fee_on_transfer, + route, + fees, + ) + + async def make_trade_output( + self, + input_token: AddressLike, + output_token: AddressLike, + qty: int | Wei, + recipient: AddressLike | None = None, + fee: int | None = None, + slippage: float | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> HexBytes: + """Make a trade by defining the qty of the output token.""" + fee = validate_fee_tier(fee=fee, version=self.version) + + if slippage is None: + slippage = self.default_slippage + + if input_token == output_token: + raise ValueError + + if self.version != 3 and fees: + fees = None + + if input_token == ETH_ADDRESS and (route is None or fees is None): + balance = await self.get_eth_balance() + need = await self._get_eth_token_output_price(output_token, qty, fee) + if balance < need: + raise InsufficientBalance(balance, need) + return await self._eth_to_token_swap_output( + output_token, qty, recipient, fee, slippage + ) + elif output_token == ETH_ADDRESS and (route is None or fees is None): + return await self._token_to_eth_swap_output( + input_token, Wei(qty), recipient, fee, slippage + ) + else: + # if is_same_address(input_token, ETH_ADDRESS): + # input_token = await self.get_weth_address() + # if is_same_address(output_token, ETH_ADDRESS): + # output_token = await self.get_weth_address() + return await self._token_to_token_swap_output( + input_token, output_token, qty, recipient, fee, slippage, route, fees + ) + + async def _eth_to_token_swap_input( + self, + output_token: AddressLike, + qty: Wei, + recipient: AddressLike | None, + fee: int, + slippage: float, + fee_on_transfer: bool = False, + ) -> HexBytes: + """Convert ETH to tokens given an input amount.""" + if output_token == ETH_ADDRESS: + raise ValueError + + eth_balance = await self.get_eth_balance() + if qty > eth_balance: + raise InsufficientBalance(eth_balance, qty) + + if self.version == 1: + token_funcs = (await self._exchange_contract(output_token)).functions + tx_params = await self._get_tx_params(qty) + func_params: list[Any] = [qty, self._deadline()] + if not recipient: + function = token_funcs.ethToTokenSwapInput(*func_params) + else: + func_params.append(recipient) + function = token_funcs.ethToTokenTransferInput(*func_params) + return await self._build_and_send_tx(function, tx_params) + + elif self.version == 2: + if recipient is None: + recipient = self.address + amount_out_min = int( + (1 - slippage) + * await self._get_eth_token_input_price(output_token, qty, fee) + ) + if fee_on_transfer: + func = self.router.functions.swapExactETHForTokensSupportingFeeOnTransferTokens + else: + func = self.router.functions.swapExactETHForTokens + return await self._build_and_send_tx( + func( + amount_out_min, + [await self.get_weth_address(), output_token], + recipient, + self._deadline(), + ), + await self._get_tx_params(qty), + ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + if fee_on_transfer: + raise ValueError("fee on transfer not supported by Uniswap v3") + + min_tokens_bought = int( + (1 - slippage) + * await self._get_eth_token_input_price(output_token, qty, fee=fee) + ) + sqrtPriceLimitX96 = 0 + + return await self._build_and_send_tx( + self.router.functions.exactInputSingle( + { + "tokenIn": await self.get_weth_address(), + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + } + ), + await self._get_tx_params(value=qty), + ) + else: + raise ValueError # pragma: no cover + + async def _token_to_eth_swap_input( + self, + input_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + fee_on_transfer: bool = False, + ) -> HexBytes: + """Convert tokens to ETH given an input amount.""" + if input_token == ETH_ADDRESS: + raise ValueError + + # Balance check + input_balance = await self.get_token_balance(input_token) + if qty > input_balance: + raise InsufficientBalance(input_balance, qty) + + if self.version == 1: + token_funcs = (await self._exchange_contract(input_token)).functions + func_params: list[Any] = [qty, 1, self._deadline()] + if not recipient: + function = token_funcs.tokenToEthSwapInput(*func_params) + else: + func_params.append(recipient) + function = token_funcs.tokenToEthTransferInput(*func_params) + return await self._build_and_send_tx(function) + elif self.version == 2: + if recipient is None: + recipient = self.address + amount_out_min = int( + (1 - slippage) + * await self._get_token_eth_input_price(input_token, qty, fee) + ) + if fee_on_transfer: + func = self.router.functions.swapExactTokensForETHSupportingFeeOnTransferTokens + else: + func = self.router.functions.swapExactTokensForETH + return await self._build_and_send_tx( + func( + qty, + amount_out_min, + [input_token, await self.get_weth_address()], + recipient, + self._deadline(), + ), + ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + if fee_on_transfer: + raise ValueError("fee on transfer not supported by Uniswap v3") + + output_token = await self.get_weth_address() + min_tokens_bought = int( + (1 - slippage) + * await self._get_token_eth_input_price(input_token, qty, fee=fee) + ) + sqrtPriceLimitX96 = 0 + + swap_data = self.router.encode_abi( + "exactInputSingle", + args=[ + ( + input_token, + output_token, + fee, + ETH_ADDRESS, + self._deadline(), + qty, + min_tokens_bought, + sqrtPriceLimitX96, + ) + ], + ) + + unwrap_data = self.router.encode_abi( + "unwrapWETH9", args=[min_tokens_bought, recipient] + ) + + # Multicall + return await self._build_and_send_tx( + self.router.functions.multicall([swap_data, unwrap_data]), + await self._get_tx_params(), + ) + else: + raise ValueError # pragma: no cover + + async def _token_to_token_swap_input( + self, + input_token: AddressLike, + output_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + fee_on_transfer: bool = False, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> HexBytes: + """Convert tokens to tokens given an input amount.""" + # Balance check + input_balance = await self.get_token_balance(input_token) + if qty > input_balance: + raise InsufficientBalance(input_balance, qty) + + if recipient is None: + recipient = self.address + + if (input_token == ETH_ADDRESS or output_token == ETH_ADDRESS) and ( + route is None or fees is None + ): + raise ValueError + + if self.version == 1: + token_funcs = (await self._exchange_contract(input_token)).functions + # TODO: This might not be correct + min_tokens_bought, min_eth_bought = await self._calculate_max_output_token( + input_token, qty, output_token + ) + func_params = [ + qty, + min_tokens_bought, + min_eth_bought, + self._deadline(), + output_token, + ] + if not recipient: + function = token_funcs.tokenToTokenSwapInput(*func_params) + else: + func_params.insert(len(func_params) - 1, recipient) + function = token_funcs.tokenToTokenTransferInput(*func_params) + return await self._build_and_send_tx(function) + elif self.version == 2: + if fee_on_transfer: + func = self.router.functions.swapExactTokensForTokensSupportingFeeOnTransferTokens + else: + func = self.router.functions.swapExactTokensForTokens + if route is None: + weth_address = await self.get_weth_address() + if is_same_address(input_token, weth_address) or is_same_address( + output_token, weth_address + ): + path = [input_token, output_token] + else: + path = [input_token, weth_address, output_token] + else: + path = route + min_tokens_bought = int( + (1 - slippage) + * await self._get_token_token_input_price( + input_token, output_token, qty, fee=fee, route=path, fees=fees + ) + ) + return await self._build_and_send_tx( + func( + qty, + min_tokens_bought, + path, + recipient, + self._deadline(), + ), + ) + elif self.version == 3: + if fee_on_transfer: + raise ValueError("fee on transfer not supported by Uniswap v3") + + min_tokens_bought = int( + (1 - slippage) + * await self._get_token_token_input_price( + input_token, output_token, qty, fee=fee, route=route, fees=fees + ) + ) + sqrtPriceLimitX96 = 0 + if route and fees: + encoded_v3_path = self.encode_v3_path(route, fees) + return await self._build_and_send_tx( + self.router.functions.exactInput( + { + "path": encoded_v3_path, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + } + ), + await self._get_tx_params(), + ) + else: + return await self._build_and_send_tx( + self.router.functions.exactInputSingle( + { + "tokenIn": input_token, + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + } + ), + await self._get_tx_params(), + ) + else: + raise ValueError # pragma: no cover + + async def _eth_to_token_swap_output( + self, + output_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + ) -> HexBytes: + """Convert ETH to tokens given an output amount.""" + if output_token == ETH_ADDRESS: + raise ValueError + + # Balance check + eth_balance = await self.get_eth_balance() + cost = await self._get_eth_token_output_price(output_token, qty, fee) + amount_in_max = Wei(int((1 + slippage) * cost)) + + # We check balance against amount_in_max rather than cost to be conservative + if amount_in_max > eth_balance: + raise InsufficientBalance(eth_balance, amount_in_max) + + if self.version == 1: + token_funcs = (await self._exchange_contract(output_token)).functions + eth_qty = await self._get_eth_token_output_price(output_token, qty) + tx_params = await self._get_tx_params(eth_qty) + func_params: list[Any] = [qty, self._deadline()] + if not recipient: + function = token_funcs.ethToTokenSwapOutput(*func_params) + else: + func_params.append(recipient) + function = token_funcs.ethToTokenTransferOutput(*func_params) + return await self._build_and_send_tx(function, tx_params) + elif self.version == 2: + if recipient is None: + recipient = self.address + eth_qty = int( + (1 + slippage) + * await self._get_eth_token_output_price(output_token, qty, fee) + ) # type: ignore + return await self._build_and_send_tx( + self.router.functions.swapETHForExactTokens( + qty, + [await self.get_weth_address(), output_token], + recipient, + self._deadline(), + ), + await self._get_tx_params(eth_qty), + ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + sqrtPriceLimitX96 = 0 + + swap_data = self.router.encode_abi( + "exactOutputSingle", + args=[ + ( + await self.get_weth_address(), + output_token, + fee, + recipient, + self._deadline(), + qty, + amount_in_max, + sqrtPriceLimitX96, + ) + ], + ) + + refund_data = self.router.encode_abi("refundETH", args=None) + + # Multicall + return await self._build_and_send_tx( + self.router.functions.multicall([swap_data, refund_data]), + await self._get_tx_params(value=amount_in_max), + ) + else: + raise ValueError + + async def _token_to_eth_swap_output( + self, + input_token: AddressLike, + qty: Wei, + recipient: AddressLike | None, + fee: int, + slippage: float, + ) -> HexBytes: + """Convert tokens to ETH given an output amount.""" + if input_token == ETH_ADDRESS: + raise ValueError + + # Balance check + input_balance = await self.get_token_balance(input_token) + cost = await self._get_token_eth_output_price(input_token, qty, fee) + amount_in_max = int((1 + slippage) * cost) + + # We check balance against amount_in_max rather than cost to be conservative + if amount_in_max > input_balance: + raise InsufficientBalance(input_balance, amount_in_max) + + if self.version == 1: + # From https://uniswap.org/docs/v1/frontend-integration/trade-tokens/ + # Is all this really necessary? Can't we just use `cost` for max_tokens? + outputAmount = qty + inputReserve = await self.get_ex_token_balance(input_token) + outputReserve = await self.get_ex_eth_balance(input_token) + + numerator = outputAmount * inputReserve * 1000 + denominator = (outputReserve - outputAmount) * 997 + inputAmount = numerator / denominator + 1 + + max_tokens = int((1 + slippage) * inputAmount) + + ex = await self._exchange_contract(input_token) + func_params: list[Any] = [qty, max_tokens, self._deadline()] + if not recipient: + function = ex.functions.tokenToEthSwapOutput(*func_params) + else: + func_params.append(recipient) + function = ex.functions.tokenToEthTransferOutput(*func_params) + return await self._build_and_send_tx(function) + elif self.version == 2: + if recipient is None: + recipient = self.address + + max_tokens = int((1 + slippage) * cost) + return await self._build_and_send_tx( + self.router.functions.swapTokensForExactETH( + qty, + max_tokens, + [input_token, await self.get_weth_address()], + recipient, + self._deadline(), + ), + ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + sqrtPriceLimitX96 = 0 + + swap_data = self.router.encode_abi( + "exactOutputSingle", + args=[ + ( + input_token, + await self.get_weth_address(), + fee, + ETH_ADDRESS, + self._deadline(), + qty, + amount_in_max, + sqrtPriceLimitX96, + ) + ], + ) + + unwrap_data = self.router.encode_abi("unwrapWETH9", args=[qty, recipient]) + + # Multicall + return await self._build_and_send_tx( + self.router.functions.multicall([swap_data, unwrap_data]), + await self._get_tx_params(), + ) + else: + raise ValueError + + async def _token_to_token_swap_output( + self, + input_token: AddressLike, + output_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> HexBytes: + """Convert tokens to tokens given an output amount. + + :param fee: TODO + """ + if (input_token == ETH_ADDRESS or output_token == ETH_ADDRESS) and ( + route is None or fees is None + ): + raise ValueError + + # Balance check + input_balance = await self.get_token_balance(input_token) + cost = await self._get_token_token_output_price( + input_token, output_token, qty, fee, route, fees + ) + amount_in_max = int((1 + slippage) * cost) + if ( + amount_in_max > input_balance + ): # We check balance against amount_in_max rather than cost to be conservative + raise InsufficientBalance(input_balance, amount_in_max) + + if self.version == 1: + token_funcs = (await self._exchange_contract(input_token)).functions + max_tokens_sold, max_eth_sold = await self._calculate_max_input_token( + input_token, qty, output_token + ) + tx_params = await self._get_tx_params() + func_params = [ + qty, + max_tokens_sold, + max_eth_sold, + self._deadline(), + output_token, + ] + if not recipient: + function = token_funcs.tokenToTokenSwapOutput(*func_params) + else: + func_params.insert(len(func_params) - 1, recipient) + function = token_funcs.tokenToTokenTransferOutput(*func_params) + return await self._build_and_send_tx(function, tx_params) + elif self.version == 2: + if recipient is None: + recipient = self.address + if route is None: + weth = await self.get_weth_address() + path = ( + [input_token, output_token] + if is_same_address(input_token, weth) + or is_same_address(output_token, weth) + else [input_token, weth, output_token] + ) + else: + path = route + cost = await self._get_token_token_output_price( + input_token, output_token, qty, fee=fee, route=path, fees=fees + ) + amount_in_max = int((1 + slippage) * cost) + return await self._build_and_send_tx( + self.router.functions.swapTokensForExactTokens( + qty, + amount_in_max, + path, + recipient, + self._deadline(), + ), + ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + sqrtPriceLimitX96 = 0 + + if route and fees: + encoded_v3_path = self.encode_v3_path(route, fees, is_exact_out=True) + return await self._build_and_send_tx( + self.router.functions.exactOutput( + { + "path": encoded_v3_path, + "recipient": recipient, + "deadline": self._deadline(), + "amountOut": qty, + "amountInMaximum": amount_in_max, + } + ), + await self._get_tx_params(), + ) + else: + return await self._build_and_send_tx( + self.router.functions.exactOutputSingle( + { + "tokenIn": input_token, + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountOut": qty, + "amountInMaximum": amount_in_max, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + }, + ), + await self._get_tx_params(), + ) + else: + raise ValueError + + # ------ Wallet balance ------------------------------------------------------------ + async def get_eth_balance(self) -> Wei: + """Get the balance of ETH for your address.""" + return await self.w3.eth.get_balance(self.address) + + async def get_token_balance(self, token: AddressLike) -> int: + """Get the balance of a token for your address.""" + _validate_address(token) + if _addr_to_str(token) == ETH_ADDRESS: + return await self.get_eth_balance() + erc20 = _load_contract_erc20(self.w3, token) + balance: int = await erc20.functions.balanceOf(self.address).call() + return balance + + # ------ ERC20 Pool ---------------------------------------------------------------- + async def get_ex_eth_balance(self, token: AddressLike) -> int: + """Get the balance of ETH in an exchange contract.""" + ex_addr: AddressLike = await self._exchange_address_from_token(token) + return await self.w3.eth.get_balance(ex_addr) + + async def get_ex_token_balance(self, token: AddressLike) -> int: + """Get the balance of a token in an exchange contract.""" + erc20 = _load_contract_erc20(self.w3, token) + balance: int = await erc20.functions.balanceOf( + await self._exchange_address_from_token(token) + ).call() + return balance + + # TODO: ADD TOTAL SUPPLY + async def get_exchange_rate(self, token: AddressLike) -> float: + """Get the current ETH/token exchange rate of the token.""" + eth_reserve = await self.get_ex_eth_balance(token) + token_reserve = await self.get_ex_token_balance(token) + return float(token_reserve / eth_reserve) + + # ------ Liquidity ----------------------------------------------------------------- + async def add_liquidity( + self, token: AddressLike, max_eth: Wei, min_liquidity: int = 1 + ) -> HexBytes: + """Add liquidity to the pool.""" + tx_params = await self._get_tx_params(max_eth) + # Add 1 to avoid rounding errors, per + # https://hackmd.io/hthz9hXKQmSyXfMbPsut1g#Add-Liquidity-Calculations + max_token = int(max_eth * await self.get_exchange_rate(token)) + 10 + func_params = [min_liquidity, max_token, self._deadline()] + function = (await self._exchange_contract(token)).functions.addLiquidity( + *func_params + ) + return await self._build_and_send_tx(function, tx_params) + + async def remove_liquidity(self, token: str, max_token: int) -> HexBytes: + """Remove liquidity from the pool.""" + func_params = [int(max_token), 1, 1, self._deadline()] + function = ( + await self._exchange_contract(self.w3.to_checksum_address(token)) + ).functions.removeLiquidity(*func_params) + return await self._build_and_send_tx(function) + + async def mint_liquidity( + self, + pool: AsyncContract, + amount_0: int, + amount_1: int, + tick_lower: int, + tick_upper: int, + deadline: int = 2**64, + ) -> TxReceipt: + """ + add liquidity to pool and mint position nft + """ + + token_0 = await pool.functions.token0().call() + token_1 = await pool.functions.token1().call() + token_0_instance = _load_contract(self.w3, abi_name="erc20", address=token_0) + token_1_instance = _load_contract(self.w3, abi_name="erc20", address=token_1) + + balance_0 = await self.get_token_balance(token_0) + balance_1 = await self.get_token_balance(token_1) + + assert balance_0 > amount_0, f"Have {balance_0}, need {amount_0}: {token_0}" + assert balance_1 > amount_1, f"Have {balance_1}, need {amount_1}: {token_1}" + + fee = await pool.functions.fee().call() + tick_lower = nearest_tick(tick_lower, fee) + tick_upper = nearest_tick(tick_upper, fee) + assert tick_lower < tick_upper, "Invalid tick range" + + *_, isInit = await pool.functions.slot0().call() + # If pool is not initialized, init pool w/ sqrt_price_x96 encoded from amount_0 & amount_1 + if isInit is False: + sqrt_pricex96 = encode_sqrt_ratioX96(amount_0, amount_1) + await pool.functions.initialize(sqrt_pricex96).transact( + {"from": _addr_to_str(self.address)} + ) + + nft_manager = self.nonFungiblePositionManager + await token_0_instance.functions.approve( + nft_manager.address, amount_0 + ).transact({"from": _addr_to_str(self.address)}) + await token_1_instance.functions.approve( + nft_manager.address, amount_1 + ).transact({"from": _addr_to_str(self.address)}) + + # TODO: add slippage param + tx_hash = await nft_manager.functions.mint( + ( + token_0, + token_1, + fee, + tick_lower, + tick_upper, + amount_0, + amount_1, + 0, + 0, + self.address, + deadline, + ) + ).transact({"from": _addr_to_str(self.address)}) + receipt = await self.w3.eth.wait_for_transaction_receipt(tx_hash) + return receipt + + # TODO: should this be multiple functions? + async def close_position( + self, + tokenId: int, + amount0Min: int = 0, + amount1Min: int = 0, + deadline: int | None = None, + ) -> TxReceipt: + """ + remove all liquidity from the position associated w/ tokenId, collect fees, and burn token. + """ + position = await self.nonFungiblePositionManager.functions.positions( + tokenId + ).call() + + if deadline is None: + deadline = self._deadline() + + # If collecting fees in ETH, fees must be precomputed to protect against reentrancy + # source: https://docs.uniswap.org/sdk/guides/liquidity/removing + + if position[2] == WETH9_ADDRESS or position[3] == WETH9_ADDRESS: + ( + amount0Min, + amount1Min, + ) = await self.nonFungiblePositionManager.functions.collect( + (tokenId, _addr_to_str(self.address), MAX_UINT_128, MAX_UINT_128) + ).call() + + tx_remove_liquidity = await ( + self.nonFungiblePositionManager.functions.decreaseLiquidity( + (tokenId, position[7], amount0Min, amount1Min, deadline) + ).transact({"from": _addr_to_str(self.address)}) + ) + await self.w3.eth.wait_for_transaction_receipt(tx_remove_liquidity) + + tx_collect_fees = await self.nonFungiblePositionManager.functions.collect( + (tokenId, _addr_to_str(self.address), MAX_UINT_128, MAX_UINT_128) + ).transact({"from": _addr_to_str(self.address)}) + await self.w3.eth.wait_for_transaction_receipt(tx_collect_fees) + + tx_burn = await self.nonFungiblePositionManager.functions.burn( + tokenId + ).transact({"from": _addr_to_str(self.address)}) + receipt = await self.w3.eth.wait_for_transaction_receipt(tx_burn) + + return receipt + + # Below two functions derived from: https://stackoverflow.com/questions/71814845/how-to-calculate-uniswap-v3-pools-total-value-locked-tvl-on-chain + def get_token0_in_pool( + self, + liquidity: float, + sqrtPrice: float, + sqrtPriceLow: float, + sqrtPriceHigh: float, + ) -> float: + sqrtPrice = max(min(sqrtPrice, sqrtPriceHigh), sqrtPriceLow) + return liquidity * (sqrtPriceHigh - sqrtPrice) / (sqrtPrice * sqrtPriceHigh) + + def get_token1_in_pool( + self, + liquidity: float, + sqrtPrice: float, + sqrtPriceLow: float, + sqrtPriceHigh: float, + ) -> float: + sqrtPrice = max(min(sqrtPrice, sqrtPriceHigh), sqrtPriceLow) + return liquidity * (sqrtPrice - sqrtPriceLow) + + # Find maximum tick of the word at the largest index (wordPos) in the tickBitmap that contains an initialized tick + def get_max_tick_from_wordpos( + self, wordPos: int, bitmap: str, tick_spacing: int, fee: int + ) -> int: + compressed_tick = wordPos << 8 + _tick = compressed_tick * tick_spacing + min_tick_in_word = nearest_tick(_tick, fee) + max_tick_in_word = min_tick_in_word + (len(bitmap) * tick_spacing) + return max_tick_in_word + + # Find minimum tick of word at the smallest index (wordPos) in the tickBitmap that contains an initialized tick + def get_min_tick_from_wordpos( + self, wordPos: int, tick_spacing: int, fee: int + ) -> int: + compressed_tick = wordPos << 8 + _tick = compressed_tick * tick_spacing + min_tick_in_word = nearest_tick(_tick, fee) + return min_tick_in_word + + # Find min or max tick in initialized tick range using the tickBitmap + async def find_tick_from_bitmap( + self, + bitmap_spacing: tuple[int, int], + pool: AsyncContract, + tick_spacing: int, + fee: int, + left: bool = True, + ) -> int | bool: + # searching to the left (finding max tick) + if left: + min_wordPos = bitmap_spacing[1] + max_wordPos = bitmap_spacing[0] + step = -1 + # searching to the right (finding min tick) + else: + min_wordPos = bitmap_spacing[0] + max_wordPos = bitmap_spacing[1] + step = 1 + + # Some fun tickBitmap hacks below. + # Iterate thru each possible wordPos (based on tick_spacing), get the bitmap "word" (basically a sub-array of the full bitmap), + # check if there is an initialized tick, derive largest (or smallest) tick in this word + # + # Since wordPos (int16 index of tickBitmap mapping) are calculated by (tick/tickspacing) >> 8, deriving tick from wordPos + # is done by (wordPos << 8)*tickSpacing. This however does not find the precise tick (only a possible tick that could map to that bitmap sub-array, or word), + # thus we must calculate the nearest viable tick depending on the tick_spacing of the pool using nearest_tick(). + # If searching for the maximum tick, we must then add-back len(bitmap)*tick_spacing as each bit in the bitmap should correspond to a tick. + + for wordPos in range(min_wordPos, max_wordPos, step): + word = await pool.functions.tickBitmap(wordPos).call() + bitmap = bin(word) + for bit in bitmap[3:]: + if int(bit) == 1: + if left: + _max_tick = self.get_max_tick_from_wordpos( + wordPos, bitmap, tick_spacing, fee + ) + return _max_tick + else: + _min_tick = self.get_min_tick_from_wordpos( + wordPos, tick_spacing, fee + ) + return _min_tick + return False + + async def get_tvl_in_pool(self, pool: AsyncContract) -> tuple[float, float]: + """ + Iterate through each tick in a pool and calculate the TVL on-chain + + Note: the output of this function may differ from what is returned by the + UniswapV3 subgraph api (https://github.com/Uniswap/v3-subgraph/issues/74) + + Params + ------ + pool: Contract + pool contract instance to find TVL + """ + pool_tick_output_types = ( + "uint128", + "int128", + "uint256", + "uint256", + "int56", + "uint160", + "uint32", + "bool", + ) + + pool_immutables = await self.get_pool_immutables(pool) + pool_state = await self.get_pool_state(pool) + fee = pool_immutables["fee"] + sqrtPrice = pool_state["sqrtPriceX96"] / (1 << 96) + + token0_liquidity = 0.0 + token1_liquidity = 0.0 + liquidity_total = 0.0 + + TICK_SPACING = _tick_spacing[fee] + BITMAP_SPACING = _tick_bitmap_range[fee] + + _max_tick = await self.find_tick_from_bitmap( + BITMAP_SPACING, pool, TICK_SPACING, fee, True + ) + _min_tick = await self.find_tick_from_bitmap( + BITMAP_SPACING, pool, TICK_SPACING, fee, False + ) + assert _max_tick is not False, "Error finding max tick" + assert _min_tick is not False, "Error finding min tick" + + Batch = namedtuple("Batch", "ticks batchResults") + ticks = [] + # Batching pool.functions.tick() calls as these are the major bottleneck to performance + for batch in list(chunks(range(_min_tick, _max_tick, TICK_SPACING), 100)): + _batch = [] + _ticks = [] + for tick in batch: + _batch.append( + ( + pool.address, + HexBytes(pool.functions.ticks(tick)._encode_transaction_data()), + ) + ) + _ticks.append(tick) + ticks.append( + Batch(_ticks, await self.multicall(_batch, pool_tick_output_types)) + ) + + for tickBatch in ticks: + tick_arr = tickBatch.ticks + for i in range(len(tick_arr)): + tick = tick_arr[i] + tickData = tickBatch.batchResults[i] + # source: https://stackoverflow.com/questions/71814845/how-to-calculate-uniswap-v3-pools-total-value-locked-tvl-on-chain + liquidityNet = tickData[1] + liquidity_total += liquidityNet + sqrtPriceLow = 1.0001 ** (tick // 2) + sqrtPriceHigh = 1.0001 ** ((tick + TICK_SPACING) // 2) + token0_liquidity += self.get_token0_in_pool( + liquidity_total, sqrtPrice, sqrtPriceLow, sqrtPriceHigh + ) + token1_liquidity += self.get_token1_in_pool( + liquidity_total, sqrtPrice, sqrtPriceLow, sqrtPriceHigh + ) + + # Correcting for each token's respective decimals + token0_decimals = await ( + _load_contract_erc20(self.w3, pool_immutables["token0"]) + .functions.decimals() + .call() + ) + token1_decimals = await ( + _load_contract_erc20(self.w3, pool_immutables["token1"]) + .functions.decimals() + .call() + ) + token0_liquidity = token0_liquidity // (10**token0_decimals) + token1_liquidity = token1_liquidity // (10**token1_decimals) + return (token0_liquidity, token1_liquidity) + + # ------ Approval Utils ------------------------------------------------------------ + async def approve( + self, token: AddressLike, max_approval: int | None = None + ) -> TxReceipt: + """Give an exchange/router max approval of a token.""" + max_approval = self.max_approval_int if not max_approval else max_approval + contract_addr = ( + self._exchange_address_from_token(token) + if self.version == 1 + else self.router_address + ) + function = _load_contract_erc20(self.w3, token).functions.approve( + contract_addr, max_approval + ) + logger.warning(f"Approving {_addr_to_str(token)}...") + tx = await self._build_and_send_tx(function) + receipt = await self.w3.eth.wait_for_transaction_receipt(tx, timeout=6000) + + # Add extra sleep to let tx propagate correctly + await asyncio.sleep(1) + return receipt + + async def _is_approved(self, token: AddressLike) -> bool: + """Check to see if the exchange and token is approved.""" + _validate_address(token) + if self.version == 1: + contract_addr = await self._exchange_address_from_token(token) + elif self.version in [2, 3]: + contract_addr = self.router_address + else: + raise ValueError + amount = await ( + _load_contract_erc20(self.w3, token) + .functions.allowance(self.address, contract_addr) + .call() + ) + return bool(amount >= self.max_approval_check_int) + + # ------ Tx Utils ------------------------------------------------------------------ + def _deadline(self) -> int: + """Get a predefined deadline. 10min by default (same as the Uniswap SDK).""" + return int(time.time()) + 10 * 60 + + async def _build_and_send_tx( + self, + function: AsyncContractFunction, + tx_params: TxParams | None = None, + ) -> HexBytes: + """Build and send a transaction.""" + if not tx_params: + tx_params = await self._get_tx_params() + + # Pre-populate gas BEFORE build_transaction to prevent web3 from calling + # eth_estimateGas internally. web3 calls eth_estimateGas during + # build_transaction when no gas is provided, which fails for contracts + # with computed jumps (e.g. Vyper v1 exchange contracts) under Anvil's + # strict EVM. use_estimate_gas=True for networks like Arbitrum where 500k + # is not a safe default. + if "gas" not in tx_params and not self.use_estimate_gas: + tx_params["gas"] = Wei(500_000) + + transaction = await function.build_transaction(tx_params) + + if "gas" not in tx_params: + # use_estimate_gas=True: run explicit estimate with 20% margin + # The Uniswap V3 UI uses 20% margin for transactions + transaction["gas"] = Wei( + int(await self.w3.eth.estimate_gas(transaction) * 1.2) + ) + + signed_txn = self.w3.eth.account.sign_transaction( + transaction, private_key=self.private_key + ) + # TODO: This needs to get more complicated if we want to support replacing a transaction + # FIXME: This does not play nice if transactions are sent from other places using the same wallet. + try: + return await self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) + finally: + logger.debug(f"nonce: {tx_params['nonce']}") + self.last_nonce = Nonce(tx_params["nonce"] + 1) + + async def _get_tx_params( + self, value: Wei = Wei(0), gas: Wei | None = None + ) -> TxParams: + """Get generic transaction parameters.""" + params: TxParams = { + "from": _addr_to_str(self.address), + "value": value, + "nonce": max( + self.last_nonce, await self.w3.eth.get_transaction_count(self.address) + ), + } + + if gas: + params["gas"] = gas + + return params + + # ------ Price Calculation Utils --------------------------------------------------- + async def _calculate_max_input_token( + self, input_token: AddressLike, qty: int, output_token: AddressLike + ) -> tuple[int, int]: + """ + For buy orders (exact output), the cost (input) is calculated. + Calculate the max input and max eth sold for a token to token output swap. + Equation from: + - https://hackmd.io/hthz9hXKQmSyXfMbPsut1g + - https://uniswap.org/docs/v1/frontend-integration/trade-tokens/ + """ + # Buy TokenB with ETH + output_amount_b = qty + input_reserve_b = await self.get_ex_eth_balance(output_token) + output_reserve_b = await self.get_ex_token_balance(output_token) + + # Cost + numerator_b = output_amount_b * input_reserve_b * 1000 + denominator_b = (output_reserve_b - output_amount_b) * 997 + input_amount_b = numerator_b / denominator_b + 1 + + # Buy ETH with TokenA + output_amount_a = input_amount_b + input_reserve_a = await self.get_ex_token_balance(input_token) + output_reserve_a = await self.get_ex_eth_balance(input_token) + + # Cost + numerator_a = output_amount_a * input_reserve_a * 1000 + denominator_a = (output_reserve_a - output_amount_a) * 997 + input_amount_a = numerator_a / denominator_a - 1 + + return int(input_amount_a), int(1.2 * input_amount_b) + + async def _calculate_max_output_token( + self, output_token: AddressLike, qty: int, input_token: AddressLike + ) -> tuple[int, int]: + """ + For sell orders (exact input), the amount bought (output) is calculated. + Similar to _calculate_max_input_token, but for an exact input swap. + """ + # TokenA (ERC20) to ETH conversion + inputAmountA = qty + inputReserveA = await self.get_ex_token_balance(input_token) + outputReserveA = await self.get_ex_eth_balance(input_token) + + # Cost + numeratorA = inputAmountA * outputReserveA * 997 + denominatorA = inputReserveA * 1000 + inputAmountA * 997 + outputAmountA = numeratorA / denominatorA + + # ETH to TokenB conversion + inputAmountB = outputAmountA + inputReserveB = await self.get_ex_token_balance(output_token) + outputReserveB = await self.get_ex_eth_balance(output_token) + + # Cost + numeratorB = inputAmountB * outputReserveB * 997 + denominatorB = inputReserveB * 1000 + inputAmountB * 997 + outputAmountB = numeratorB / denominatorB + + return int(outputAmountB), int(1.2 * outputAmountA) + + # ------ Helpers ------------------------------------------------------------ + + # Batch contract function calls to speed up large on-chain data queries + async def multicall( + self, + encoded_functions: Sequence[tuple[ChecksumAddress, bytes]], + output_types: Sequence[str], + ) -> list[Any]: + """ + Calls aggregate() on Uniswap Multicall2 contract + + Params + ------ + encoded_functions : Sequence[Tuple[ChecksumAddress, bytes]] + array of tuples containing address of contract and byte-encoded transaction data + + output_types: Sequence[str] + array of solidity output types for decoding (e.g. uint256, bool, etc.) + + returns decoded results + """ + params = [ + {"target": target, "callData": callData} + for target, callData in encoded_functions + ] + _, results = await self.multicall2.functions.aggregate(params).call( + block_identifier="latest" + ) + decoded_results = [ + self.w3.codec.decode(output_types, multicall_result) + for multicall_result in results + ] + normalized_results = [ + map_abi_data(BASE_RETURN_NORMALIZERS, output_types, decoded_result) + for decoded_result in decoded_results + ] + return normalized_results + + async def get_token( + self, address: AddressLike, abi_name: str = "erc20" + ) -> ERC20Token: + """ + Retrieves metadata from the ERC20 contract of a given token, like its name, symbol, and decimals. + """ + # FIXME: This function should always return the same output for the same input + # and would therefore benefit from caching + if address == "0x0000000000000000000000000000000000000000": + # This isn't exactly right, but for all intents and purposes, + # ETH is treated as a ERC20 by Uniswap. + return ERC20Token( + address=address, + name="ETH", + symbol="ETH", + decimals=18, + ) + token_contract = _load_contract(self.w3, abi_name, address=address) + try: + _name = await token_contract.functions.name().call() + _symbol = await token_contract.functions.symbol().call() + decimals = await token_contract.functions.decimals().call() + except ValueError as e: + logger.warning( + f"Exception occurred while trying to get token {_addr_to_str(address)}: {e}" + ) + raise InvalidToken(address) + try: + name = _name.decode() + except (ValueError, AttributeError): + name = _name + try: + symbol = _symbol.decode() + except (ValueError, AttributeError): + symbol = _symbol + return ERC20Token(symbol, address, name, decimals) + + async def get_weth_address(self) -> ChecksumAddress: + """Retrieves the WETH address from the contracts (which may vary between chains).""" + if self.version == 1: + address: ChecksumAddress = self.w3.to_checksum_address(WETH9_ADDRESS) + elif self.version == 2: + # Contract calls should always return checksummed addresses + address = await self.router.functions.WETH().call() + elif self.version == 3: + address = await self.router.functions.WETH9().call() + else: + raise ValueError # pragma: no cover + + # Mainnet WETH9 address + # WETH9_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + # assert address == WETH9_ADDRESS, "WETH address mismatch" + + return address + + async def get_pool_instance( + self, token_0: AddressLike, token_1: AddressLike, fee: int = 3_000 + ) -> AsyncContract: + """ + Returns an instance of a pool contract for a given token pair and fee. + Requires pair [token_in, token_out, fee] has a direct pool. + Will return 0x0 address if pool does not exist. + """ + + assert token_0 != token_1, "Token addresses cannot be the same" + fee = validate_fee_tier(fee=fee, version=self.version) + + pool_address = await self.factory_contract.functions.getPool( + token_0, token_1, fee + ).call() + assert pool_address != ETH_ADDRESS, "0 address returned. Pool does not exist" + pool_instance = _load_contract( + self.w3, abi_name="uniswap-v3/pool", address=pool_address + ) + + return pool_instance + + async def create_pool_instance( + self, token_0: AddressLike, token_1: AddressLike, fee: int = 3_000 + ) -> AsyncContract: + """ + Creates and returns UniswapV3 Pool instance. Requires that fee is valid and no similar pool already exists. + """ + address = _addr_to_str(self.address) + assert token_0 != token_1, "Token addresses cannot be the same" + fee = validate_fee_tier(fee=fee, version=self.version) + + tx = await self.factory_contract.functions.createPool( + token_0, token_1, fee + ).transact({"from": address}) + receipt = await self.w3.eth.wait_for_transaction_receipt(tx) + + event_logs = await self.factory_contract.events.PoolCreated().process_receipt( + receipt + ) + pool_address = event_logs[0]["args"]["pool"] + pool_instance = _load_contract( + self.w3, abi_name="uniswap-v3/pool", address=pool_address + ) + + return pool_instance + + async def get_pool_immutables(self, pool: AsyncContract) -> dict: + """ + Fetch on-chain pool data. + """ + pool_immutables = { + "factory": await pool.functions.factory().call(), + "token0": await pool.functions.token0().call(), + "token1": await pool.functions.token1().call(), + "fee": await pool.functions.fee().call(), + "tickSpacing": await pool.functions.tickSpacing().call(), + "maxLiquidityPerTick": await pool.functions.maxLiquidityPerTick().call(), + } + + return pool_immutables + + async def get_pool_state(self, pool: AsyncContract) -> dict: + """ + Fetch on-chain pool state. + """ + liquidity = await pool.functions.liquidity().call() + slot = await pool.functions.slot0().call() + pool_state = { + "liquidity": liquidity, + "sqrtPriceX96": slot[0], + "tick": slot[1], + "observationIndex": slot[2], + "observationCardinality": slot[3], + "observationCardinalityNext": slot[4], + "feeProtocol": slot[5], + "unlocked": slot[6], + } + + return pool_state + + async def get_liquidity_positions(self) -> list[int]: + """ + Enumerates liquidity position tokens owned by address. + Returns array of token IDs. + """ + positions: list[int] = [] + number_of_positions = await self.nonFungiblePositionManager.functions.balanceOf( + _addr_to_str(self.address) + ).call() + if number_of_positions > 0: + for idx in range(number_of_positions): + position = ( + await self.nonFungiblePositionManager.functions.tokenOfOwnerByIndex( + _addr_to_str(self.address), idx + ).call() + ) + positions.append(position) + return positions + + # FIXME: mint call reverting - likely to do w/ passing struct args to contract function call + # FIXME: mint call reverting - likely due to handling of token amounts + + async def mint_position( + self, pool: AsyncContract, amount0: int, amount1: int + ) -> HexBytes: + pool_immutables = await self.get_pool_immutables(pool) + + token0 = pool_immutables["token0"] + token1 = pool_immutables["token1"] + fee = pool_immutables["fee"] + + positionManager = self.nonFungiblePositionManager + + approve0 = _load_contract_erc20(self.w3, token0).functions.approve( + self.positionManager_addr, amount0 + ) + logger.warning(f"Approving {_addr_to_str(token0)}...") + tx0 = await self._build_and_send_tx(approve0) + await self.w3.eth.wait_for_transaction_receipt(tx0, timeout=6000) + + approve1 = _load_contract_erc20(self.w3, token1).functions.approve( + self.positionManager_addr, amount1 * 1000 + ) + logger.warning(f"Approving {_addr_to_str(token1)}...") + tx1 = await self._build_and_send_tx(approve1) + await self.w3.eth.wait_for_transaction_receipt(tx1, timeout=6000) + + # tx_mint = pool.functions.mint(self.address, MIN_TICK, MAX_TICK, amount0,'').transact(); + + position = positionManager.encode_abi( + "mint", + args=[ + { + "token0": token0, + "token1": token1, + "fee": fee, + "tickLower": MIN_TICK, + "tickUpper": MAX_TICK, + "amount0Desired": amount0, + "amount1Desired": amount1, + "amount0Min": 0, + "amount1Min": 0, + "recipient": _addr_to_str(self.address), + "deadline": self._deadline(), + } + ], + ) + print(position) + + multicall = await positionManager.functions.multicall([position]).transact( + {"from": _addr_to_str(self.address), "gas": Wei(417918)} + ) + + print(multicall) + # mint_position = positionManager.functions.mint({'token0':token0,'token1':token1,'fee':fee,'tickLower':MIN_TICK,'tickUpper':MAX_TICK, + # 'amount0Desired':amount0,'amount1Desired':amount1,'amount0Min':0,'amount1Min':0,'recipient':_addr_to_str(self.address),'deadline':self._deadline() + # }) + + # mint_tx = self._build_and_send_tx(mint_position) + # self.w3.eth.wait_for_transaction_receipt(mint_tx, timeout=6000) + # + # tx2 = self._build_and_send_tx(multicall,) + # self.w3.eth.wait_for_transaction_receipt(tx2, timeout=6000) + + # position = positionManager.functions.mint().buildTransaction() + # print(position['data']) + + return multicall + + def encode_v3_path( + self, route: list[AddressLike], fees: list[int], is_exact_out: bool = False + ) -> bytes: + """ + Encodes a Uniswap V3 path for use in swap and quote functions. + + :param route : A list of token addresses representing the swap route. The length of this list must be one greater than the length of the `fees` list. + :param fees : A list of fee tiers corresponding to each hop in the route. Each fee tier should be an integer representing the fee in hundredths of a bip (e.g., 3000 for 0.3%). + :param is_exact_out : A boolean indicating whether the swap is an exact output swap. If True, the path will be encoded in reverse order. + + :return : A bytes object representing the encoded path. + """ + if len(route) < 2: + raise ValueError( + "Route must contain at least two token addresses. " + f"Got route length {len(route)}." + ) + if len(route) != len(fees) + 1: + raise ValueError( + "Route length must be one greater than fees length. " + f"Got route length {len(route)} and fees length {len(fees)}." + ) + + path = b"" + _route = route.copy() + _fees = fees.copy() + if is_exact_out: + # For exact output swaps, the path is encoded in reverse order + _route.reverse() + _fees.reverse() + + for i in range(len(_fees)): + path += self.w3.to_bytes(hexstr=HexStr(_addr_to_str(_route[i]))) + _fees[ + i + ].to_bytes(3, "big") + path += self.w3.to_bytes(hexstr=HexStr(_addr_to_str(_route[-1]))) + + return path + + async def get_raw_price( + self, token_in: AddressLike, token_out: AddressLike, fee: int | None = None + ) -> float: + """ + Returns current price for pair of tokens [token_in, token_out] regrading liquidity that is being locked in the pool + Parameter `fee` is required for V3 only, can be omitted for V2 + Requires pair [token_in, token_out] having direct pool + """ + fee = validate_fee_tier(fee=fee, version=self.version) + + if token_in == ETH_ADDRESS: + token_in = await self.get_weth_address() + if token_out == ETH_ADDRESS: + token_out = await self.get_weth_address() + + if self.version == 2: + params: Iterable[ChecksumAddress | int | None] = [ + self.w3.to_checksum_address(token_in), + self.w3.to_checksum_address(token_out), + ] + pair_token = await self.factory_contract.functions.getPair(*params).call() + token_in_erc20 = _load_contract_erc20( + self.w3, self.w3.to_checksum_address(token_in) + ) + token_in_balance = int( + await token_in_erc20.functions.balanceOf( + self.w3.to_checksum_address(pair_token) + ).call() + ) + token_in_decimals = (await self.get_token(token_in)).decimals + token_in_balance = token_in_balance / (10**token_in_decimals) + + token_out_erc20 = _load_contract_erc20( + self.w3, self.w3.to_checksum_address(token_out) + ) + token_out_balance = int( + await token_out_erc20.functions.balanceOf( + self.w3.to_checksum_address(pair_token) + ).call() + ) + token_out_decimals = (await self.get_token(token_out)).decimals + token_out_balance = token_out_balance / (10**token_out_decimals) + + raw_price = token_out_balance / token_in_balance + else: + params = [ + self.w3.to_checksum_address(token_in), + self.w3.to_checksum_address(token_out), + fee, + ] + pool_address = await self.factory_contract.functions.getPool(*params).call() + pool_contract = _load_contract( + self.w3, abi_name="uniswap-v3/pool", address=pool_address + ) + # t0 = pool_contract.functions.token0().call() + t1 = await pool_contract.functions.token1().call() + if t1.lower() == token_in.lower(): + den0 = (await self.get_token(token_in)).decimals + den1 = (await self.get_token(token_out)).decimals + else: + den0 = (await self.get_token(token_out)).decimals + den1 = (await self.get_token(token_in)).decimals + sqrtPriceX96 = (await pool_contract.functions.slot0().call())[0] + raw_price = (sqrtPriceX96 * sqrtPriceX96 * 10**den1 >> (96 * 2)) / ( + 10**den0 + ) + if t1.lower() == token_in.lower(): + raw_price = 1 / raw_price + return raw_price + + async def estimate_price_impact( + self, + token_in: AddressLike, + token_out: AddressLike, + amount_in: int, + fee: int, + route: list[AddressLike] | None = None, + ) -> float: + """ + Returns the estimated price impact as a positive float (0.01 = 1%). + + NOTE: Work-in-progress. + + See ``examples/price_impact.py`` for an example which uses this. + """ + try: + price_small = await self.get_raw_price( + token_in, + token_out, + fee=fee, + ) + except (ArithmeticError, BadFunctionCallOutput): + # ArithmeticError is raised when `token_in` amount in the pool equals 0. + # BadFunctionCallOutput is raised when the pool's contract for given `(token_in, token_out, fee)` hasn't been deployed + return 1 + + if price_small == 0: + # Occurs when `token_out` amount in the pool equals 0 + return 1 + try: + cost_amount = await self.get_price_input( + token_in, token_out, amount_in, fee=fee, route=route + ) + except ContractLogicError: + # ContractLogicError is raised when the pool's contract for given `(token_in, token_out, fee)` hasn't been deployed. + # As `get_price_input()` uses UniswapV3Quoter for getting prices, that contract raises such exception in this situation. + return 1 + price_amount = ( + cost_amount + / (amount_in / (10 ** (await self.get_token(token_in)).decimals)) + ) / 10 ** (await self.get_token(token_out)).decimals + + # 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 ------------------------------------------------------------------ + def get_fee_maker(self) -> float: + """Get the maker fee.""" + return 0 + + def get_fee_taker(self) -> float: + """Get the taker fee.""" + return 0.003 + + # ---- Old v1 utils ---- + + async def _exchange_address_from_token( + self, token_addr: AddressLike + ) -> AddressLike: + ex_addr: AddressLike = await self.factory_contract.functions.getExchange( + token_addr + ).call() + # TODO: What happens if the token doesn't have an exchange/doesn't exist? + # Should probably raise an Exception (and test it) + return ex_addr + + async def _token_address_from_exchange(self, exchange_addr: AddressLike) -> Address: + token_addr: Address = await ( + (await self._exchange_contract(ex_addr=exchange_addr)) + .functions.tokenAddress(exchange_addr) + .call() + ) + return token_addr + + async def _exchange_contract( + self, + token_addr: AddressLike | None = None, + ex_addr: AddressLike | None = None, + ) -> AsyncContract: + if not ex_addr and token_addr: + ex_addr = await self._exchange_address_from_token(token_addr) + if ex_addr is None: + raise InvalidToken(token_addr) + abi_name = "uniswap-v1/exchange" + contract = _load_contract(self.w3, abi_name=abi_name, address=ex_addr) + logger.info(f"Loaded exchange contract {contract} at {contract.address}") + return contract + + async def _get_all_tokens(self) -> list[ERC20Token]: + """ + Retrieves all token pairs. + + Note: This is a *very* expensive operation and might therefore not work properly. + """ + # FIXME: This is a very expensive operation, would benefit greatly from caching. + tokenCount = await self.factory_contract.functions.tokenCount().call() + tokens = [] + for i in range(tokenCount): + address = await self.factory_contract.functions.getTokenWithId(i).call() + if address == "0x0000000000000000000000000000000000000000": + # Token is ETH + continue + token = await self.get_token(address) + tokens.append(token) + return tokens + + +def _load_contract(w3: AsyncWeb3, abi_name: str, address: AddressLike) -> AsyncContract: + address = Web3.to_checksum_address(address) + return w3.eth.contract(address=address, abi=_load_abi(abi_name)) + + +def _load_contract_erc20(w3: AsyncWeb3, address: AddressLike) -> AsyncContract: + return _load_contract(w3, "erc20", address) diff --git a/uniswap/asyncuniswap4.py b/uniswap/asyncuniswap4.py new file mode 100644 index 00000000..2c4709df --- /dev/null +++ b/uniswap/asyncuniswap4.py @@ -0,0 +1,3508 @@ +import asyncio +import logging +import os +import time +from dataclasses import astuple +from decimal import Decimal + +import eth_abi.abi +from eth_abi import encode +from eth_abi.packed import encode_packed +from web3 import AsyncWeb3, Web3 +from web3.contract import AsyncContract +from web3.contract.async_contract import AsyncContractFunction +from web3.exceptions import BadFunctionCallOutput, ContractLogicError, NameNotFound +from web3.types import ( + HexBytes, + Nonce, + TxParams, + Wei, +) + +from .constants import ( + ETH_ADDRESS, + Q96, + ZERO_HOOK, + _netid_to_name, + _permit2_contract_addresses_v4, + _poolmanager_contract_addresses_v4, + _position_descriptor_contract_addresses_v4, + _position_manager_contract_addresses_v4, + _quoter_contract_addresses_v4, + _reserves_lens_contract_addresses_v4, + _router_contract_addresses_v4, + _stateview_contract_addresses_v4, + universal_router_commands, + universal_router_commands_abis, + v4_actions, + v4_actions_abis, +) +from .exceptions import InvalidToken +from .token import ERC20Token +from .types import ( + AddressLike, + ModifyLiquidityParams, + PathKey, + PermitBatch, + PermitSingle, + PoolKey, + SwapParams, +) +from .util import ( + _addr_to_str, + _load_abi, + _str_to_addr, + get_sqrt_ratio_at_tick, + realised_fee_percentage, +) + +logger = logging.getLogger(__name__) + + +class AsyncUniswap4: + """ + Wrapper around Uniswap v4 contracts. + """ + + w3: AsyncWeb3 + address: AddressLike + last_nonce: Nonce + _nonce_lock: asyncio.Lock + + def __init__( + self, + address: str | AddressLike, + private_key: str | None = None, + provider: str | None = None, + web3: AsyncWeb3 | None = None, + max_slippage: float = 0.01, + gas_limit: float = 250000.0, + gas_price: float = 1.80, + priority_fee: float = 1.0, + post_merge: bool = True, + ) -> None: + """ + :param address: The public address of the ETH wallet to use. + :param private_key: The private key of the ETH wallet to use. + :param provider: Can be optionally set to a Web3 provider URI. If none set, will fall back to the PROVIDER environment variable, or web3 if set. + :param web3: Can be optionally set to a custom Web3 instance. + :param max_slippage: Maximum slippage for a trade, as a float (0.01 is 1%). + :param gas_limit: Maximum gas amount allocated for transactions. + :param gas_price: Cost per unit of gas, in GWei. + :param priority_fee: Amount of ETH to pay to the block producers, in GWei. Affects tx position in the block, the bigger value, the higher position is. + :param post_merge: True is for post-Merge transations, False for legacy ones. + """ + + self.address: AddressLike = ( + _str_to_addr(address) if isinstance(address, str) else address + ) + self.private_key = private_key + + self.max_slippage = max_slippage + + if web3: + self.w3 = web3 + # Derive sync provider URL from the supplied async web3 so we don't + # require PROVIDER in the environment when a web3 instance is given. + if not provider: + _ep = getattr(self.w3.provider, "endpoint_uri", None) + provider = str(_ep) if _ep is not None else os.environ["PROVIDER"] + self.provider = provider + else: + self.provider = provider or os.environ["PROVIDER"] + self.w3 = AsyncWeb3( + AsyncWeb3.AsyncHTTPProvider( + self.provider, request_kwargs={"timeout": 60} + ) + ) + temp_w3 = Web3(Web3.HTTPProvider(self.provider, request_kwargs={"timeout": 60})) + self.last_nonce = temp_w3.eth.get_transaction_count(self.address) + self._nonce_lock = asyncio.Lock() + + # This code automatically approves you for trading on the exchange. + # max_approval is to allow the contract to exchange on your behalf. + # max_approval_check checks that current approval is above a reasonable + # number + # The program cannot check for max_approval each time because it + # decreases with each trade. + self.max_approval_hex = f"0x{64 * 'f'}" + self.max_approval_int = int(self.max_approval_hex, 16) + self.max_approval_check_hex = f"0x{15 * '0'}{49 * 'f'}" + self.max_approval_check_int = int(self.max_approval_check_hex, 16) + self.gas_limit = gas_limit + self.gas_price = gas_price + self.post_merge = post_merge + self.priority_fee = priority_fee + + chain_id = int(temp_w3.net.version) + self.net_id = chain_id + if self.net_id in _netid_to_name: + self.net_name = _netid_to_name[self.net_id] + if self.net_name not in _quoter_contract_addresses_v4: + raise ValueError(f"Netid {self.net_id} is not supported yet.") + else: + raise ValueError(f"Unknown netid: {self.net_id}") + + logger.info(f"Using {self.w3} ('{self.net_name}', netid: {self.net_id})") + quoter_address = _quoter_contract_addresses_v4[self.net_name] + router_address = _router_contract_addresses_v4[self.net_name] + stateview_address = _stateview_contract_addresses_v4[self.net_name] + permit2_address = _permit2_contract_addresses_v4[self.net_name] + position_descriptor_address = _position_descriptor_contract_addresses_v4[ + self.net_name + ] + pool_manager_address = _poolmanager_contract_addresses_v4[self.net_name] + position_manager_address = _position_manager_contract_addresses_v4[ + self.net_name + ] + reserves_lens_address = _reserves_lens_contract_addresses_v4[self.net_name] + + """ + NOTE: Following exception handling relates to the testnets only, as production contracts are always deployed. + Specifically, PositionDescriptor is not deployed on Sepolia and Unichain Sepolia yet, so loading it will raise an error. + However, this is a periphery helper which is not critical for core functionality. + """ + try: + self.quoter_address = _str_to_addr(quoter_address) + self.quoter = _load_contract( + self.w3, abi_name="uniswap-v4/quoter", address=self.quoter_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading quoter contract: {e}") + + try: + self.router_address = _str_to_addr(router_address) + self.router = _load_contract( + self.w3, abi_name="uniswap-v4/router", address=self.router_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading router contract: {e}") + + try: + self.stateview_address = _str_to_addr(stateview_address) + self.stateview = _load_contract( + self.w3, abi_name="uniswap-v4/stateview", address=self.stateview_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading stateview contract: {e}") + + try: + self.permit2_address = _str_to_addr(permit2_address) + self.permit2 = _load_contract( + self.w3, abi_name="uniswap-v4/permit2", address=self.permit2_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading permit2 contract: {e}") + + try: + self.position_descriptor_address = _str_to_addr(position_descriptor_address) + self.position_descriptor = _load_contract( + self.w3, + abi_name="uniswap-v4/pos_descriptor", + address=self.position_descriptor_address, + ) + except NameNotFound as e: + logger.error( + f"Error occurred while loading position descriptor contract: {e}" + ) + + try: + self.pool_manager_address = _str_to_addr(pool_manager_address) + self.pool_manager = _load_contract( + self.w3, + abi_name="uniswap-v4/poolmanager", + address=self.pool_manager_address, + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading pool manager contract: {e}") + + try: + self.position_manager_address = _str_to_addr(position_manager_address) + self.position_manager = _load_contract( + self.w3, + abi_name="uniswap-v4/pos_manager", + address=self.position_manager_address, + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading position manager contract: {e}") + + try: + self.reserves_lens_address = _str_to_addr(reserves_lens_address) + self.reserves_lens = _load_contract( + self.w3, + abi_name="uniswap-v4/reserves_lens", + address=self.reserves_lens_address, + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading reserves lens contract: {e}") + + # Approvals + async def approve( + self, + token: AddressLike, + max_approval: int | None = None, + delay_interval: int | None = 7, + approve_position_manager: bool = False, + ) -> HexBytes: + """Approve the router to spend a token on the user's behalf, or set up a permit for the position manager to pull the token from the user's wallet. For ETH, the router can pull from the user's wallet directly, so no approval is necessary. + + :param token: The address of the token to approve. + :param max_approval: Optional. The maximum amount to approve. If not set, will approve a maximum possible amount. + :param delay_interval: Optional. Seconds to wait between two approval transactions. Defaults to 7. Values less than 1 are treated as default. + :param approve_position_manager: Optional. Whether to approve the position manager to spend the token. Defaults to False. + """ + + # If the token is not ETH, approve the router to spend it. For ETH, the router can pull from the user's wallet directly, so no approval is necessary. + if _addr_to_str(token) != ETH_ADDRESS: + max_approval = self.max_approval_int if not max_approval else max_approval + function = self.erc20_contract(token).functions.approve( + _addr_to_str(self.permit2_address), max_approval + ) + logger.info(f"Approving {_addr_to_str(token)} for PERMIT2...") + tx = await self._build_and_send_tx(function) + if delay_interval is None or delay_interval < 1: + delay_interval = 7 + await asyncio.sleep(delay_interval) + else: + raise ValueError("ETH needs no approval.") + # Give an exchange/router max approval for a token. + max_approval = 2**100 - 1 + expiration: int = 10**12 + logger.info(f"Setting permit for {_addr_to_str(token)} at router contract...") + function = self.permit2.functions.approve( + _str_to_addr(token), self.router_address, max_approval, expiration + ) + tx = await self._build_and_send_tx(function) + + if approve_position_manager: + await asyncio.sleep(delay_interval) + max_approval = 2**100 - 1 + expiration = 10**12 + logger.info( + f"Setting permit for {_addr_to_str(token)} at position manager contract..." + ) + function = self.permit2.functions.approve( + _str_to_addr(token), + self.position_manager_address, + max_approval, + expiration, + ) + tx = await self._build_and_send_tx(function) + return tx + + async def approval(self, token: AddressLike) -> int: + """Returns the current allowance for the router to spend a token on the user's behalf. Note that this is not the allowance of the token itself, but the allowance set in the Permit2 contract for the router to spend the token.""" + # [0]=current allowance, [1]=allowance expiration [2]=current nonce + result = int( + ( + await self.permit2.functions.allowance( + self.address, token, self.router.address + ).call() + )[0] + ) + return result + + # Transaction parameters customization + # Gas limit + def get_gas_limit(self) -> float: + """Returns the current gas limit for transactions.""" + return self.gas_limit + + def set_gas_limit(self, gas_limit: float) -> None: + """Sets the gas limit for transactions.""" + self.gas_limit = gas_limit + + # Gas price in GWei + def get_gas_price(self) -> float: + """Returns the current gas price in GWei.""" + return self.gas_price + + def set_gas_price(self, gas_price: float) -> None: + """Sets the gas price in GWei.""" + self.gas_price = gas_price + + # Priority fee in GWei + def get_gas_priorityfee(self) -> float: + """Returns the current priority fee in GWei.""" + return self.priority_fee + + def set_gas_priorityfee(self, priority_fee: float) -> None: + """Sets the priority fee in GWei.""" + self.priority_fee = priority_fee + + # Slippage + def get_max_slippage(self) -> float: + """Returns the current maximum slippage as a float (0.01 is 1%).""" + return self.max_slippage + + def set_max_slippage(self, max_slippage: float) -> None: + """Sets the maximum slippage as a float (0.01 is 1%).""" + self.max_slippage = max_slippage + + # Nonce management + async def update_last_nonce(self) -> None: + """Updates the last nonce to the current nonce of the wallet. This can be used to resync the nonce if transactions have been sent outside of this class or custom nonce is used.""" + self.last_nonce = await self.w3.eth.get_transaction_count(self.address) + + # StateView methods + async def stateview_get_fee_growth_globals( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ) -> dict: + """ + Retrieves the global fee growth of a pool. + """ + if token0.lower() > token1.lower(): + token0, token1 = token1, token0 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + fee_growth_globals: dict = await self.stateview.functions.getFeeGrowthGlobals( + pool_id + ).call() + return_value = { + "feeGrowthGlobal0": fee_growth_globals[0], + "feeGrowthGlobal1": fee_growth_globals[1], + } + return return_value + + async def stateview_get_fee_growth_inside( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick_lower: int, + tick_upper: int, + ) -> dict: + """ + Calculates the fee growth inside a tick range of a pool + """ + if token0.lower() > token1.lower(): + token0, token1 = token1, token0 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + fee_growth_inside: dict = await self.stateview.functions.getFeeGrowthInside( + pool_id, tick_lower, tick_upper + ).call() + return_value = { + "feeGrowthInside0X128": fee_growth_inside[0], + "feeGrowthInside1X128": fee_growth_inside[1], + } + return return_value + + async def stateview_get_liquidity( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ) -> int: + """Retrieves the total liquidity of a pool.""" + if token0.lower() > token1.lower(): + token0, token1 = token1, token0 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + liquidity: int = await self.stateview.functions.getLiquidity(pool_id).call() + return liquidity + + async def stateview_get_position_info( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + owner: str, + tick_lower: int, + tick_upper: int, + token_id: int, + ) -> dict: + """ + Retrieves position info in a pool. + + :param token_id: TokenID of the correspoding NFT + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + salt = HexBytes(token_id.to_bytes(32, byteorder="big")) + position_info: dict = await self.stateview.functions.getPositionInfo( + pool_id, owner, tick_lower, tick_upper, salt + ).call() + return_value = { + "liquidity": position_info[0], + "feeGrowthInside0LastX128": position_info[1], + "feeGrowthInside1LastX128": position_info[2], + } + return return_value + + async def stateview_get_slot0( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ) -> dict: + """ + Returns current state of the pool. + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + slot: dict = await self.stateview.functions.getSlot0(pool_id).call() + return_value = { + "sqrtPriceX96": slot[0], + "tick": slot[1], + "protocolFee": slot[2], + "lpFee": slot[3], + } + return return_value + + async def stateview_get_tick_bitmap( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick: int, + ) -> int: + """ + Retrieves the tick bitmap of a pool at a specific tick. + + :param tick: MUST be int16 + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + tick_bitmap: int = await self.stateview.functions.getTickBitmap( + pool_id, tick + ).call() + return tick_bitmap + + async def stateview_get_tick_fee_growth_outside( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick: int, + ) -> dict: + """ + Retrieves the fee growth outside a tick range of a pool + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + fee_growth_outside: dict = ( + await self.stateview.functions.getTickFeeGrowthOutside(pool_id, tick).call() + ) + return_value = { + "feeGrowthOutside0X128": fee_growth_outside[0], + "feeGrowthOutside1X128": fee_growth_outside[1], + } + return return_value + + async def stateview_get_tick_pool_info( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick: int, + ) -> dict: + """ + Retrieves the tick information of a pool at a specific tick. + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + tick_info: dict = await self.stateview.functions.getTickInfo( + pool_id, tick + ).call() + return_value = { + "liquidityGross": tick_info[0], + "liquidityNet": tick_info[1], + "feeGrowthOutside0X128": tick_info[2], + "feeGrowthOutside1X128": tick_info[3], + } + return return_value + + # ReservesLens methods + async def reserves_lens_get_pool_tvl( + self, pool_key: PoolKey, custom_provider: str = "" + ) -> dict: + """ + Retrieves the total value locked (TVL) of a pool. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_key: The PoolKey object representing the pool. + :param custom_provider: The custom provider address, empty string for default. + :returns: A dictionary containing the reserves of the pool. + """ + if custom_provider == "": + reserves: dict = await self.reserves_lens.functions.getPoolTVL( + _addr_to_str(self.pool_manager_address), astuple(pool_key) + ).call() + else: + reserves = await self.reserves_lens.functions.getPoolTVL( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + custom_provider, + ).call() + return_value = { + "coreAmount0": reserves[0], + "coreAmount1": reserves[1], + "hookReserves0": reserves[2], + "hookReserves1": reserves[3], + "hookEffective0": reserves[4], + "hookEffective1": reserves[5], + "sqrtPriceX96": reserves[6], + "tick": reserves[7], + "activeLiquidity": reserves[8], + "blockNumber": reserves[9], + "statsProvider": reserves[10], + "hookPermissions": reserves[11], + "hasCustomAccounting": reserves[12], + "statsStatus": reserves[13], + } + return return_value + + async def reserves_lens_get_pool_tvl_batch( + self, pool_keys: list[PoolKey], custom_provider: list[str] | None = None + ) -> list[dict]: + """ + Retrieves the total value locked (TVL) of multiple pools in a batch. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_keys: A list of PoolKey objects representing the pools. + :param custom_provider: The custom provider addresses for each pool. If None, will use default for all pools. + :returns: A list of dictionaries containing the reserves of each pool. + """ + if custom_provider is None: + custom_provider = [""] * len(pool_keys) + + reserves_list: list = await self.reserves_lens.functions.getPoolTVLBatch( + _addr_to_str(self.pool_manager_address), + [astuple(pool_key) for pool_key in pool_keys], + ).call() + else: + reserves_list = await self.reserves_lens.functions.getPoolTVLBatch( + _addr_to_str(self.pool_manager_address), + [astuple(pool_key) for pool_key in pool_keys], + custom_provider, + ).call() + + return_value = [] + for reserves in reserves_list: + return_value.append( + { + "coreAmount0": reserves[0], + "coreAmount1": reserves[1], + "hookReserves0": reserves[2], + "hookReserves1": reserves[3], + "hookEffective0": reserves[4], + "hookEffective1": reserves[5], + "sqrtPriceX96": reserves[6], + "tick": reserves[7], + "activeLiquidity": reserves[8], + "blockNumber": reserves[9], + "statsProvider": reserves[10], + "hookPermissions": reserves[11], + "hasCustomAccounting": reserves[12], + "statsStatus": reserves[13], + } + ) + + return return_value + + async def reserves_lens_get_tvl_paged( + self, + pool_key: PoolKey, + cursor: bytes, + custom_provider: str = "", + max_reads: int = 0, + ) -> tuple[dict, bytes, bool]: + """ + Retrieves the total value locked (TVL) of multiple pools in a paged manner. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_key: A list of PoolKey objects representing the pools. + :param cursor: The cursor for paged retrieval. + :param custom_provider: The custom provider address, empty string for default. + :param max_reads: The maximum number of reads to perform using custom provider. + :returns: A tuple containing the reserves dictionary, the next cursor, and a boolean indicating if the paged retrieval is done. + """ + if custom_provider == "": + ( + reserves_tuple, + next_cursor, + done, + ) = await self.reserves_lens.functions.getPoolTVLPaged( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + cursor, + ).call() + else: + ( + reserves_tuple, + next_cursor, + done, + ) = await self.reserves_lens.functions.getPoolTVLPaged( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + custom_provider, + cursor, + max_reads, + ).call() + + reserves: dict = { + "coreAmount0": reserves_tuple[0], + "coreAmount1": reserves_tuple[1], + "hookReserves0": reserves_tuple[2], + "hookReserves1": reserves_tuple[3], + "hookEffective0": reserves_tuple[4], + "hookEffective1": reserves_tuple[5], + "sqrtPriceX96": reserves_tuple[6], + "tick": reserves_tuple[7], + "activeLiquidity": reserves_tuple[8], + "blockNumber": reserves_tuple[9], + "statsProvider": reserves_tuple[10], + "hookPermissions": reserves_tuple[11], + "hasCustomAccounting": reserves_tuple[12], + "statsStatus": reserves_tuple[13], + } + + return_value = (reserves, next_cursor, done) + return return_value + + async def reserves_lens_get_populated_ticks_in_word( + self, pool_key: PoolKey, word_position: int + ) -> list[dict]: + """ + Retrieves the populated ticks in a specific word of a pool. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_key: The PoolKey object representing the pool. + :param word_position: The position of the word to retrieve. + :returns: A list of dictionaries containing the populated ticks. + """ + results: list = await self.reserves_lens.functions.getPopulatedTicksInWord( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + word_position, + ).call() + return_value: list[dict] = [] + for result in results: + return_value.append( + { + "tick": result[0], + "liquidityNet": result[1], + "liquidityGross": result[2], + } + ) + return return_value + + # PositionDescriptor methods + async def position_descriptor_get_currency_ratio_priority( + self, currency: str + ) -> int: + """ + For certain currencies on mainnet, the smaller the currency, the higher the priority. + And those with the higher priority values (more positive values) will be in the numerator of the price ratio + + :returns: The priority of a currency. + """ + ratio_priority: int = int( + await self.position_descriptor.functions.currencyRatioPriority( + currency + ).call() + ) + return_value = ratio_priority + return return_value + + async def position_descriptor_get_flip_ratio( + self, currency0: str, currency1: str + ) -> bool: + """ + :returns: True if currency0 has higher priority than currency1 + """ + flip_ratio: bool = bool( + await self.position_descriptor.functions.flipRatio( + currency0, currency1 + ).call() + ) + return_value = flip_ratio + return return_value + + async def position_descriptor_get_native_currency_label(self) -> str: + """ + :returns: The label for the native currency as a string + """ + native_currency_label: str = str( + await self.position_descriptor.functions.nativeCurrencyLabel().call() + ) + return_value = native_currency_label + return return_value + + async def position_descriptor_get_pool_manager(self) -> str: + """ + :returns: PoolManager address as a string + """ + pool_manager: str = str( + await self.position_descriptor.functions.poolManager().call() + ) + return_value = pool_manager + return return_value + + async def position_descriptor_get_token_uri( + self, pos_manager: str, token_id: int + ) -> str: + """ + Produces the URI describing a particular token ID + Note this URI may be a data: URI with the JSON contents directly inlined + + :returns: The URI of the ERC721-compliant metadata + """ + token_uri: str = str( + await self.position_descriptor.functions.tokenURI( + pos_manager, token_id + ).call() + ) + return_value = token_uri + return return_value + + async def position_descriptor_get_wrapped_native_address(self) -> str: + """ + :returns: The wrapped native currency address as a string + """ + wrapped_native_address: str = str( + await self.position_descriptor.functions.wrappedNative().call() + ) + return_value = wrapped_native_address + return return_value + + # PositionManager methods + # Read methods + async def position_manager_get_domain_separator( + self, + ) -> bytes: + """ + :returns: The domain separator for the current chain; bytes32 + """ + domain_separator: bytes = bytes( + await self.position_manager.functions.DOMAIN_SEPARATOR().call() + ) + return_value = domain_separator + return return_value + + async def position_manager_get_weth9( + self, + ) -> str: + """ + :returns: The wrapped native token address + """ + weth9: str = str(await self.position_manager.functions.WETH9().call()) + return_value = weth9 + return return_value + + async def position_manager_get_balance_of(self, address: str) -> int: + """ + :returns: The number of tokens in owner's address. + """ + balance: int = int( + await self.position_manager.functions.balanceOf(address).call() + ) + return_value = balance + return return_value + + async def position_manager_get_approved(self, token_id: int) -> str: + """ + :returns: The account approved for a token. + """ + operator: str = str( + await self.position_manager.functions.getApproved(token_id).call() + ) + return_value = operator + return return_value + + async def position_manager_get_pool_and_position_info(self, token_id: int) -> dict: + """ + :returns: The PoolKey class object and position info of a position + """ + ( + pool_key_tuple, + info, + ) = await self.position_manager.functions.getPoolAndPositionInfo( + token_id + ).call() + pool_key: PoolKey = PoolKey(*pool_key_tuple) + return_value = { + "poolKey": pool_key, + "info": info, + } + return return_value + + async def position_manager_get_position_liquidity(self, token_id: int) -> int: + """ + :returns: The liquidity of a position + """ + position_liquidity: int = int( + await self.position_manager.functions.getPositionLiquidity(token_id).call() + ) + return_value = position_liquidity + return return_value + + async def position_manager_get_is_approved_for_all( + self, owner: str, operator: str + ) -> bool: + """ + :returns: True if the operator is allowed to manage all of the assets of owner + """ + is_approved_for_all: bool = bool( + await self.position_manager.functions.isApprovedForAll( + owner, operator + ).call() + ) + return_value = is_approved_for_all + return return_value + + async def position_manager_get_msg_sender( + self, + ) -> str: + """ + :returns: address considered executor of the actions + + The other context functions, _msgData and _msgValue, are not supported by this contract. + In many contracts this will be the address that calls the initial entry point + that calls `_executeActions` `msg.sender` shouldn't be used, as this will be + the v4 pool manager contract that calls `unlockCallback` + If using ReentrancyLock.sol, this function can return _getLocker() + """ + msg_sender: str = str(await self.position_manager.functions.msgSender().call()) + return_value = msg_sender + return return_value + + async def position_manager_get_name( + self, + ) -> str: + """ + :returns: The name of the PositionManager token + """ + name: str = str(await self.position_manager.functions.name().call()) + return_value = name + return return_value + + async def position_manager_get_next_token_id( + self, + ) -> int: + """ + :returns: The ID that will be used for the next minted liquidity position + """ + next_token_id: int = int( + await self.position_manager.functions.nextTokenId().call() + ) + return_value = next_token_id + return return_value + + async def position_manager_get_nonces(self, owner: str, word: int) -> int: + """ + :returns: Mapping of nonces consumed by each address, where a nonce is a single bit on the 256-bit bitmap + """ + bitmap: int = int( + await self.position_manager.functions.nonces(owner, word).call() + ) + return_value = bitmap + return return_value + + async def position_manager_get_owner_of(self, token_id: int) -> str: + """ + :returns: The owner of the position for a given token ID + """ + owner: str = str(await self.position_manager.functions.ownerOf(token_id).call()) + return_value = owner + return return_value + + async def position_manager_get_permit2( + self, + ) -> str: + """ + :returns: The Permit2 contract to forward approvals + """ + permit2: str = str(await self.position_manager.functions.permit2().call()) + return_value = permit2 + return return_value + + async def position_manager_get_pool_keys(self, pool_id_trunc: bytes) -> PoolKey: + """ + :param pool_id_trunc: The truncated ID of the pool, first 25 bytes of common pool_id + :returns: The PoolKey class object for a given token ID + """ + pool_keys_tuple = await self.position_manager.functions.poolKeys( + pool_id_trunc + ).call() + pool_keys: PoolKey = PoolKey(*pool_keys_tuple) + return_value = pool_keys + return return_value + + async def position_manager_get_position_info(self, token_id: int) -> int: + """ + :returns: The position info for a given token ID + """ + position_info: int = int( + await self.position_manager.functions.positionInfo(token_id).call() + ) + return_value = position_info + return return_value + + async def position_manager_get_subscriber(self, token_id: int) -> str: + """ + :returns: The subscriber of the position for a given token ID + """ + subscriber: str = str( + await self.position_manager.functions.subscriber(token_id).call() + ) + return_value = subscriber + return return_value + + async def position_manager_get_is_support_interface( + self, + interface_id: bytes, + ) -> bool: + """ + :param interface_id: The interface ID to check; should be 'bytes4' + :returns: True if specifeid interface is supported by the PositionManager contract + """ + if len(interface_id) != 4: + raise ValueError("interface_id should be 4 bytes long") + is_supported: bool = bool( + await self.position_manager.functions.supportsInterface(interface_id).call() + ) + return_value = is_supported + return return_value + + async def position_manager_get_symbol( + self, + ) -> str: + """ + :returns: The symbol of the PositionManager token + """ + symbol: str = str(await self.position_manager.functions.symbol().call()) + return_value = symbol + return return_value + + async def position_manager_get_token_descriptor( + self, + ) -> str: + """ + :returns: The address of the PositionDescriptor contract as a string + """ + token_descriptor: str = str( + await self.position_manager.functions.tokenDescriptor().call() + ) + return_value = token_descriptor + return return_value + + async def position_manager_get_position_uri(self, token_id: int) -> str: + """ + :returns: The URI of the position manager's ERC721-compliant metadata for a given token ID + """ + uri: str = str(await self.position_manager.functions.tokenURI(token_id).call()) + return_value = uri + return return_value + + async def position_manager_get_unsubscribe_gas_limit(self) -> int: + """ + :returns: The gas limit used when unsubscribing from a position. + """ + unsubscribe_gas_limit: int = ( + await self.position_manager.functions.unsubscribeGasLimit().call() + ) + return_value = unsubscribe_gas_limit + return return_value + + # Write methods + async def position_manager_approve(self, spender: str, token_id: int) -> HexBytes: + """ + Change or reaffirm the approved address for an NFT + Zero address removes existing approval. + """ + function = self.position_manager.functions.approve(spender, token_id) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def position_manager_initialize_pool( + self, pool_key: PoolKey, sqrt_price_x96: int, payable_amount: int + ) -> HexBytes: + """ + Initialize a Uniswap v4 Pool with the given parameters. + """ + function = self.position_manager.functions.initializePool( + astuple(pool_key), sqrt_price_x96 + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_modify_liquidities( + self, unlock_data: bytes, deadline: int, payable_amount: int + ) -> HexBytes: + """ + Unlocks Uniswap v4 PoolManager and batches actions for modifying liquidity + """ + function = self.position_manager.functions.modifyLiquidities( + unlock_data, deadline + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_modify_liquidities_without_unlock( + self, actions: bytes, params: list[bytes], payable_amount: int + ) -> HexBytes: + """ + Batches actions for modifying liquidity without unlocking v4 PoolManager + + This must be called by a contract that has already unlocked the v4 PoolManager + """ + function = self.position_manager.functions.modifyLiquiditiesWithoutUnlock( + actions, params + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_multicall( + self, data: list[bytes], payable_amount: int + ) -> HexBytes: + """ + Call multiple functions in the current contract in a single transaction, with the possibility of sending ETH along with the calls. + """ + function = self.position_manager.functions.multicall(data) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_permit( + self, + spender: str, + token_id: int, + deadline: int, + nonce: int, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Approve of a specific token ID for spending by spender via signature + """ + function = self.position_manager.functions.permit( + spender, token_id, deadline, nonce, signature + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_permit2_single( + self, + owner: str, + permit_single: PermitSingle, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Allows forwarding a single permit to permit2 + """ + function = self.position_manager.functions.permit( + owner, astuple(permit_single), signature + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_permit2_batch( + self, + owner: str, + permit_batch: PermitBatch, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Allows forwarding batch permits to permit2 + """ + function = self.position_manager.functions.permit( + owner, astuple(permit_batch), signature + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_permit_for_all( + self, + owner: str, + operator: str, + approved: bool, + deadline: int, + nonce: int, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Set an operator with full permission to an owner's tokens via signature + """ + function = self.position_manager.functions.permitForAll( + owner, operator, approved, deadline, nonce, signature + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_revoke_nonce( + self, nonce: int, payable_amount: int + ) -> HexBytes: + """ + Revoke a nonce by spending it, preventing it from being used again + """ + function = self.position_manager.functions.revokeNonce(nonce) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_safe_transfer_from( + self, from_addr: str, to_addr: str, token_id: int, payable_amount: int + ) -> HexBytes: + """ + Transfer a position from one address to another + """ + function = self.position_manager.functions.safeTransferFrom( + from_addr, to_addr, token_id + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_safe_transfer_from_with_data( + self, + from_addr: str, + to_addr: str, + token_id: int, + data: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Transfer a position from one address to another with additional data + """ + function = self.position_manager.functions.safeTransferFrom( + from_addr, to_addr, token_id, data + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_set_approval_for_all( + self, operator: str, approved: bool, payable_amount: int + ) -> HexBytes: + """ + Enable or disable approval for a third party ("operator") to manage all of `msg.sender`'s assets + """ + function = self.position_manager.functions.setApprovalForAll(operator, approved) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_subscribe( + self, token_id: int, new_subscriber: str, data: bytes, payable_amount: int + ) -> HexBytes: + """ + Enables the subscriber to receive notifications for a respective position + """ + function = self.position_manager.functions.subscribe( + token_id, new_subscriber, data + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_transfer_from( + self, from_addr: str, to_addr: str, token_id: int, payable_amount: int + ) -> HexBytes: + """ + Overrides solmate transferFrom in case a notification to subscribers is needed + """ + function = self.position_manager.functions.transferFrom( + from_addr, to_addr, token_id + ) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def position_manager_unsubscribe( + self, token_id: int, payable_amount: int + ) -> HexBytes: + """ + Removes the subscriber from receiving notifications for a respective position + """ + function = self.position_manager.functions.unsubscribe(token_id) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + # PoolManager methods + # Read methods + async def pool_manager_get_allowance( + self, owner: str, spender: str, token_id: int + ) -> int: + """ + Spender allowance of an id. + """ + allowance: int = int( + await self.pool_manager.functions.allowance(owner, spender, token_id).call() + ) + return_value = allowance + return return_value + + async def pool_manager_get_balance_of(self, address: str, token_id: int) -> int: + """ + The number of tokens in owner's address. + """ + balance: int = int( + await self.pool_manager.functions.balanceOf(address, token_id).call() + ) + return_value = balance + return return_value + + async def pool_manager_get_extsload(self, slot: bytes) -> bytes: + """ + Called by external contracts to access granular pool state + """ + value: bytes = await self.pool_manager.functions.extsload(slot).call() + return_value = value + return return_value + + async def pool_manager_get_extsload_sequence( + self, start_slot: bytes, slots_count: int + ) -> list[bytes]: + """ + Called by external contracts to access a sequence of storage slots + """ + value: list[bytes] = await self.pool_manager.functions.extsload( + start_slot, slots_count + ).call() + return_value = value + return return_value + + async def pool_manager_get_extsload_sparse(self, slots: list[bytes]) -> list[bytes]: + """ + Called by external contracts to access a sparse set of storage slots + """ + value: list[bytes] = await self.pool_manager.functions.extsload(slots).call() + return_value = value + return return_value + + async def pool_manager_get_exttload_sparse(self, slots: list[bytes]) -> list[bytes]: + """ + Called by external contracts to access sparse transient pool state + """ + value: list[bytes] = await self.pool_manager.functions.exttload(slots).call() + return_value = value + return return_value + + async def pool_manager_get_exttload(self, slot: bytes) -> bytes: + """ + Called by external contracts to access transient storage of the contract + """ + value: bytes = await self.pool_manager.functions.exttload(slot).call() + return_value = value + return return_value + + async def pool_manager_get_is_operator(self, owner: str, operator: str) -> bool: + """ + Checks if a spender is approved by an owner as an operator + """ + is_operator: bool = await self.pool_manager.functions.isOperator( + owner, operator + ).call() + return_value = is_operator + return return_value + + async def pool_manager_get_owner(self) -> str: + """ + Retrieve the contract owner. + """ + owner: str = str(await self.pool_manager.functions.owner().call()) + return_value = owner + return return_value + + async def pool_manager_get_protocol_fee_controller(self) -> str: + """ + Returns the current protocol fee controller address + """ + protocol_fee_controller: str = str( + await self.pool_manager.functions.protocolFeeController().call() + ) + return_value = protocol_fee_controller + return return_value + + async def pool_manager_get_protocol_fees_accrued(self, address: str) -> int: + """ + Given a currency address, returns the protocol fees accrued in that currency. + """ + protocol_fees_accrued: int = int( + await self.pool_manager.functions.protocolFeesAccrued(address).call() + ) + return_value = protocol_fees_accrued + return return_value + + async def pool_manager_get_supports_interface(self, interface_id: bytes) -> bool: + """ + Checks if a given interface ID is supported by the contract + + :param interface_id: The interface ID to check; should be `bytes4` + :returns: True if specifeid interface is supported by the PoolManager contract + """ + if len(interface_id) != 4: + raise ValueError("interface_id should be 4 bytes long") + supports_interface: bool = bool( + await self.pool_manager.functions.supportsInterface(interface_id).call() + ) + return_value = supports_interface + return return_value + + # Write methods + async def pool_manager_approve( + self, spender: str, token_id: int, amount: int + ) -> HexBytes: + """ + Approves an amount of an id to a spender. + """ + function = self.pool_manager.functions.approve(spender, token_id, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_burn( + self, from_addr: str, token_id: int, amount: int + ) -> HexBytes: + """ + Called by the user to move value from ERC6909 balance. + """ + function = self.pool_manager.functions.burn(from_addr, token_id, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_clear(self, currency: str, amount: int) -> HexBytes: + """ + !!!WARNING!!! - Any currency that is cleared, will be non-retrievable, and locked in the contract permanently. + A call to clear will zero out a positive balance WITHOUT a corresponding transfer. + This could be used to clear a balance that is considered dust. + Additionally, the amount must be the exact positive balance. + This is to enforce that the caller is aware of the amount being cleared. + """ + function = self.pool_manager.functions.clear(currency, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_collect_protocol_fees( + self, recipient: str, currency: str, amount: int + ) -> HexBytes: + """ + Collects the protocol fees for a given recipient and currency, returning the amount collected + This will revert if the contract is unlocked + """ + function = self.pool_manager.functions.collectProtocolFees( + recipient, currency, amount + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_donate( + self, pool_key: PoolKey, amount0: int, amount1: int, hook_data: bytes + ) -> HexBytes: + """ + Donate the given currency amounts to the in-range liquidity providers of a pool + """ + function = self.pool_manager.functions.donate( + astuple(pool_key), amount0, amount1, hook_data + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_initialize( + self, pool_key: PoolKey, sqrt_price_x96: int + ) -> HexBytes: + """ + Initialize the state for a given pool ID. + """ + function = self.pool_manager.functions.initialize( + astuple(pool_key), sqrt_price_x96 + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_mint( + self, to_addr: str, token_id: int, amount: int + ) -> HexBytes: + """ + Called by the user to move value into ERC6909 balance. + """ + function = self.pool_manager.functions.mint(to_addr, token_id, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_modify_liquidity( + self, + pool_key: PoolKey, + liquidity_params: ModifyLiquidityParams, + hook_data: bytes, + ) -> HexBytes: + """ + Modify the liquidity for the given pool. + """ + function = self.pool_manager.functions.modifyLiquidity( + astuple(pool_key), astuple(liquidity_params), hook_data + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_set_operator( + self, operator: str, approved: bool + ) -> HexBytes: + """ + Sets or removes an operator for the caller. + """ + function = self.pool_manager.functions.setOperator(operator, approved) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_set_protocol_fee( + self, pool_key: PoolKey, new_protocol_fee: int + ) -> HexBytes: + """ + Sets the protocol fee for the given pool. + """ + function = self.pool_manager.functions.setProtocolFee( + astuple(pool_key), new_protocol_fee + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_set_protocol_fee_controller( + self, controller: str + ) -> HexBytes: + """ + Sets a new protocol fee controller. + """ + function = self.pool_manager.functions.setProtocolFeeController(controller) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_settle(self, payable_amount: int) -> HexBytes: + """ + Called by the user to pay what is owed. + """ + function = self.pool_manager.functions.settle() + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def pool_manager_settle_for( + self, recipient: str, payable_amount: int + ) -> HexBytes: + """ + Called by the user to pay on behalf of another address. + """ + function = self.pool_manager.functions.settleFor(recipient) + tx = await self._build_and_send_tx( + function, await self._get_tx_params(value=payable_amount) + ) + return tx + + async def pool_manager_swap( + self, pool_key: PoolKey, params: SwapParams, hook_data: bytes + ) -> HexBytes: + """ + Swap against the given pool. + """ + function = self.pool_manager.functions.swap( + astuple(pool_key), astuple(params), hook_data + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_sync(self, currency: str) -> HexBytes: + """ + Writes the current ERC20 balance of the specified currency to transient storage. + This is used to checkpoint balances for the manager and derive deltas for the caller. + This MUST be called before any ERC20 tokens are sent into the contract, see documentation for more details. + """ + function = self.pool_manager.functions.sync(currency) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_take( + self, currency: str, to_addr: str, amount: int + ) -> HexBytes: + """ + Called by the user to net out some value owed to the user. + """ + function = self.pool_manager.functions.take(currency, to_addr, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_transfer( + self, to_addr: str, token_id: int, amount: int + ) -> HexBytes: + """ + Transfers an amount of an id from the caller to a receiver. + """ + function = self.pool_manager.functions.transfer(to_addr, token_id, amount) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_transfer_from( + self, sender: str, receiver: str, token_id: int, amount: int + ) -> HexBytes: + """ + Transfers an amount of an id from a sender to a receiver. + """ + function = self.pool_manager.functions.transferFrom( + sender, receiver, token_id, amount + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_transfer_ownership(self, new_owner: str) -> HexBytes: + """ + Transfers ownership of the contract to a new owner. + """ + function = self.pool_manager.functions.transferOwnership(new_owner) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_unlock(self, data: bytes) -> HexBytes: + """ + All interactions on the contract that account deltas require unlocking. + A caller that calls `unlock` must implement `IUnlockCallback(msg.sender).unlockCallback(data)`, + where they interact with the remaining functions on this contract. + """ + function = self.pool_manager.functions.unlock(data) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + async def pool_manager_update_dynamic_lp_fee( + self, pool_key: PoolKey, new_dynamic_lp_fee: int + ) -> HexBytes: + """ + Updates the pools lp fees for the a pool that has enabled dynamic lp fees. + """ + function = self.pool_manager.functions.updateDynamicLPFee( + astuple(pool_key), new_dynamic_lp_fee + ) + tx = await self._build_and_send_tx(function, await self._get_tx_params()) + return tx + + # Tokens price functions + async def get_token_token_spot_price( + self, + token0: str, + token1: str, + fee: int = 500, + tick_spacing: int = 10, + hooks: str = ZERO_HOOK, + ) -> float: + """ + :param token0: The first token in the pair. + :param token1: The second token in the pair. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :return: The current spot price for token to token trades. + """ + + if token0.lower() < token1.lower(): + den0 = (await self.get_token(_str_to_addr(token0))).decimals + den1 = (await self.get_token(_str_to_addr(token1))).decimals + zero_for_one = True + else: + den0 = (await self.get_token(_str_to_addr(token1))).decimals + den1 = (await self.get_token(_str_to_addr(token0))).decimals + zero_for_one = False + + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + spot_price_x96: int = ( + await self.stateview_get_slot0(token0, token1, fee, tick_spacing, hooks) + )["sqrtPriceX96"] + + spot_price: float = (spot_price_x96 * spot_price_x96 * 10**den0 >> (96 * 2)) / ( + 10**den1 + ) + if not zero_for_one: + spot_price = 1 / spot_price + return spot_price + + # Estimates slippage for the given amount of token0 + async def estimate_price_impact( + self, + token0: str, + token1: str, + qty: int, + fee: int = 500, + tick_spacing: int = 10, + hooks: str = ZERO_HOOK, + hook_data: bytes = b"", + ) -> float: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token0` to be sold. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :return: the estimated price impact as a positive float (0.01 = 1%). + + See ``examples/v4_examples.py`` for an example which uses this. + + NOTE: Does not support multi-hop swaps, should be used for single-hop swaps only. + """ + + try: + spot_price = await self.get_token_token_spot_price( + token0, token1, fee, tick_spacing, hooks + ) + except (ArithmeticError, BadFunctionCallOutput): + # ArithmeticError is raised when `token0` amount in the pool + # equals 0. + # BadFunctionCallOutput is raised when the pool for + # given `(token0, token1, fee)` doesn't exist + return 1 + + if spot_price == 0: + # Occurs when `token1` amount in the pool equals 0 + return 1 + try: + quote_amount = await self.get_quote_exact_input_single( + token0, token1, qty, fee, tick_spacing, hooks, hook_data + ) + except ContractLogicError: + # ContractLogicError is raised when the pool's contract for given + # `(token0, token1, fee)` hasn't been deployed. + return 1 + price = ( + quote_amount + / (qty / (10 ** (await self.get_token(_str_to_addr(token0))).decimals)) + ) / 10 ** (await self.get_token(_str_to_addr(token1))).decimals + + # calculate and subtract the realised fees from the price impact. See: + # https://github.com/uniswap-python/uniswap-python/issues/310 + price_impact_with_fees: float = (spot_price - price) / spot_price + fee_realised_percentage: float = realised_fee_percentage(fee, qty) + price_impact_real: float = price_impact_with_fees - fee_realised_percentage + return price_impact_real + + # Quoter methods + # Read methods + async def get_quote_exact_input_single( + self, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str = ZERO_HOOK, + hook_data: bytes = b"", + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token0` to be sold. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :return: Quote for token to token single hop trades with an exact input. + """ + if token0.lower() < token1.lower(): + zero_for_one = True + else: + zero_for_one = False + token0, token1 = token1, token0 + pool_key = (token0, token1, fee, tick_spacing, hooks) + # [0]=The output quote [1]=estimated gas units used for the swap + quote_amount: int = ( + await self.quoter.functions.quoteExactInputSingle( + (pool_key, zero_for_one, qty, hook_data) + ).call() + )[0] + return quote_amount + + async def get_quote_exact_input( + self, + token_exact: str, + qty: int, + route: list[PoolKey], + ) -> int: + """ + :param token_exact: The token for which the qty parameter is specified. + :param qty: The amount of the token_exact to be swapped. + :param route: The list of PoolKeys representing the path of the swap, starting with the pool containing the input token and ending with the pool containing the output token. + :return: Quote for token to token multi-hop trades with an exact input. + """ + encoded_route = self.encode_path_keys_input(route, token_exact) + + # [0]=The output quote [1]=estimated gas units used for the swap + quote_amount: int = ( + await self.quoter.functions.quoteExactInput( + ( + token_exact, + [astuple(path_key) for path_key in encoded_route], + qty, + ) + ).call() + )[0] + return quote_amount + + async def get_quote_exact_output_single( + self, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str = ZERO_HOOK, + hook_data: bytes = b"", + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token1` to be bought. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :return: Quote for token to token single hop trades with an exact output. + """ + if token0.lower() < token1.lower(): + zero_for_one = True + else: + zero_for_one = False + token1, token0 = token0, token1 + + pool_key = ( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + # [0]=The input quote [1]=estimated gas units used for the swap + quote_amount: int = ( + await self.quoter.functions.quoteExactOutputSingle( + (pool_key, zero_for_one, qty, hook_data) + ).call() + )[0] + return quote_amount + + async def get_quote_exact_output( + self, + token_exact: str, + qty: int, + route: list[PoolKey], + ) -> int: + """ + :param token_exact: The token for which the qty parameter is specified, either the input or output token depending on the quote type. + :param qty: The amount of the token_exact to be swapped. For an exact output quote, this is the amount of output token desired. + :param route: The list of PoolKeys representing the path of the swap in reverse order, starting with the pool containing the output token and ending with the pool containing the input token. + :return: Quote for token to token multi-hop trades with an exact output. + """ + + encoded_route = self.encode_path_keys_output(route, token_exact) + quote_amount: int = ( + await self.quoter.functions.quoteExactOutput( + ( + token_exact, + [astuple(path_key) for path_key in encoded_route], + qty, + ) + ).call() + )[0] + return quote_amount + + # Market price functions for selling `qty` amount of `token0` to buy `token1` + async def get_price_input( + self, + token0: str, + token1: str, + qty: int, + fee: int | None = None, + tick_spacing: int | None = None, + hooks: str | None = ZERO_HOOK, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token0` to be sold. + :param fee: Optional. The fee tier for the swap. + + :param tick_spacing: Optional. The tick spacing for the swap. + :param hooks: Optional. The hooks for the swap. + :param hook_data: Optional. The hook data for the swap. + :param route: Optional. The route for the swap. The first element should be the pool containing `token0` and the last element should be the pool containing `token1`. + :return: Returns the exact amount of `token1` that can be received for `qty` amount of `token0` at the current price. + + If `route` is provided, it will be used for the quote. Otherwise, `fee` and `tick_spacing` must be provided for a single hop quote. + """ + result: int = 0 + if route is None: + if fee is None or tick_spacing is None: + raise ValueError( + "fee and tick_spacing parameters must be provided for single hop quotes" + ) + result = await self.get_quote_exact_input_single( + token0, + token1, + qty, + fee, + tick_spacing, + hooks, # type: ignore[arg-type] + hook_data, # type: ignore[arg-type] + ) + else: + result = await self.get_quote_exact_input(token0, qty, route) + return result + + async def get_price_output( + self, + token0: str, + token1: str, + qty: int, + fee: int | None = None, + tick_spacing: int | None = None, + hooks: str | None = ZERO_HOOK, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token1` to be bought. + :param fee: Optional. The fee tier for the swap. + :param tick_spacing: Optional. The tick spacing for the swap. + :param hooks: Optional. The hooks for the swap. + :param hook_data: Optional. The hook data for the swap. + :param route: Optional. The route for the swap in reverse order. The first element should be the pool containing `token1` and the last element should be the pool containing `token0`. + :return: Returns the exact amount of `token0` required to buy `qty` amount of `token1` at the current price. + + If `route` is provided, it will be used for the quote. Otherwise, `fee` and `tick_spacing` must be provided for a single hop quote. + """ + result: int = 0 + if route is None: + if fee is None or tick_spacing is None or hooks is None: + raise ValueError( + "fee, tick_spacing, and hooks parameters must be provided for single hop quotes" + ) + result = await self.get_quote_exact_output_single( + token0, + token1, + qty, + fee, + tick_spacing, + hooks, + hook_data, # type: ignore[arg-type] + ) + else: + result = await self.get_quote_exact_output(token1, qty, route) + return result + + # Swap functions + async def token_to_token_swap_exact_input( + self, + input_token: str, + qty: int, + qtycap: int, + output_token: str, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes = b"", + min_hop_price_x_36: int = 0, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param qty: The amount of `input_token` to be sold. + :param qtycap: The minimum amount of `output_token` that must be bought for the swap to succeed. + :param output_token: The token to be bought. + :param fee: The fee for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :param min_hop_price_x_36: Optional. The minimum price for each hop in the route, expressed as an integer. + :param custom_nonce: Optional. The nonce to use for the transaction. + + Swaps an exact amount of `input_token` for a minimum amount of `output_token`, + reverting if the amount of `output_token` received is less than `qtycap`. + """ + min_tokens_bought: int = int((1 - self.max_slippage) * qtycap) + + ether_amount: int = 0 + if input_token == ETH_ADDRESS: + ether_amount = qty + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8"], + [ + v4_actions["SWAP_EXACT_IN_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ) + + # SETTING PARAMS + if input_token.lower() < output_token.lower(): + zero_for_one = True + token0, token1 = input_token, output_token + else: + zero_for_one = False + token0, token1 = output_token, input_token + exact_input_single_params: bytes = encode( + v4_actions_abis["SWAP_EXACT_IN_SINGLE"], + [ + ( + (token0, token1, fee, tick_spacing, hooks), + zero_for_one, + qty, + min_tokens_bought, + min_hop_price_x_36, + hook_data, + ) + ], + ) + settle_all_params: bytes = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, qty], + ) + take_all_params: bytes = encode( + v4_actions_abis["TAKE_ALL"], + [output_token, min_tokens_bought], + ) + + # ENCODING DATA + params = [exact_input_single_params, settle_all_params, take_all_params] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return await self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + async def token_to_token_swap_input( + self, + input_token: str, + qty: int, + qtycap: int, + route: list[PoolKey], + min_hop_price_x_36: list[int] = [], + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param qty: The amount of `input_token` to be sold. + :param qtycap: The minimum amount of `output_token` that must be bought for the swap to succeed. + :param route: The list of PoolKeys representing the path of the swap. The first element should be the pool containing the `input_token` and the last element should be the pool containing the `output_token`. + :param min_hop_price_x_36: Optional. A list of minimum prices for each hop in the route, expressed as a list of integers. + :param custom_nonce: Optional. The nonce to use for the transaction. + + Swaps an exact amount of `input_token` for a minimum amount of `output_token` through a specified multi-hop route, + reverting if the amount of `output_token` received is less than `qtycap`. + """ + min_tokens_bought: int = int((1 - self.max_slippage) * qtycap) + + encoded_route = self.encode_path_keys_input(route, input_token) + + ether_amount: int = 0 + + if input_token == ETH_ADDRESS: + ether_amount = qty + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_IN, SETTLE_ALL, TAKE_ALL + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8"], + [ + v4_actions["SWAP_EXACT_IN"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ) + + # SETTING PARAMS + exact_input_params: bytes = encode( + v4_actions_abis["SWAP_EXACT_IN"], + [ + ( + input_token, + [astuple(path_key) for path_key in encoded_route], + min_hop_price_x_36, + qty, + min_tokens_bought, + ) + ], + ) + settle_all_params: bytes = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, qty], + ) + take_all_params: bytes = encode( + v4_actions_abis["TAKE_ALL"], + [ + _addr_to_str(encoded_route[-1].intermediate_currency), # type: ignore[arg-type] + min_tokens_bought, + ], + ) + + # ENCODING DATA + params = [exact_input_params, settle_all_params, take_all_params] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return await self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + async def token_to_token_swap_exact_output( + self, + input_token: str, + qty: int, + qtycap: int, + output_token: str, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes = b"", + min_hop_price_x_36: int = 0, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param qty: The amount of `output_token` to be bought. + :param qtycap: The maximum amount of `input_token` that can be sold for the trade to proceed. + :param output_token: The token to be bought. + :param fee: The fee for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :param min_hop_price_x_36: Optional. The minimum price for each hop in the route, expressed as an integer. + :param custom_nonce: Optional. The nonce for the transaction. + """ + + amount_in_max: int = int((1 + self.max_slippage) * qtycap) + + ether_amount: int = 0 + if input_token == ETH_ADDRESS: + ether_amount = amount_in_max + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_OUT_SINGLE, SETTLE_ALL, TAKE_ALL, TAKE + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + args=[ + v4_actions["SWAP_EXACT_OUT_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + v4_actions["TAKE"], + ], + ) + # SETTING PARAMS + if input_token.lower() < output_token.lower(): + zero_for_one = True + token0, token1 = input_token, output_token + else: + zero_for_one = False + token0, token1 = output_token, input_token + exact_output_single_params = encode( + v4_actions_abis["SWAP_EXACT_OUT_SINGLE"], + [ + ( + ( + token0, + token1, + fee, + tick_spacing, + hooks, + ), + zero_for_one, + qty, + amount_in_max, + min_hop_price_x_36, + hook_data, + ) + ], + ) + settle_all_params = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, amount_in_max], + ) + take_all_params = encode( + v4_actions_abis["TAKE_ALL"], + [output_token, qty], + ) + take_input_params = encode( + v4_actions_abis["TAKE"], [input_token, _addr_to_str(self.address), 0] + ) + + # ENCODING DATA + params = [ + exact_output_single_params, + settle_all_params, + take_all_params, + take_input_params, + ] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return await self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + async def token_to_token_swap_output( + self, + output_token: str, + qty: int, + qtycap: int, + route: list[PoolKey], + min_hop_price_x_36: list[int] = [], + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param output_token: The token to be bought. + :param qty: The amount of `output_token` to be bought. + :param qtycap: The maximum amount of `input_token` that can be sold for the trade to proceed. + :param route: The route for the swap in reverse order. The first element should be the pool containing `output_token` and the last element should be the pool containing `input_token`. + :param min_hop_price_x_36: Optional. The minimum price for each hop in the route, expressed as a list of integers. + :param custom_nonce: Optional. The nonce for the transaction. + + Swaps a maximum amount of `input_token` for an exact amount of `output_token` through a specified multi-hop route, + reverting if the amount of `input_token` required is more than `qtycap`. + """ + + amount_in_max: int = int((1 + self.max_slippage) * qtycap) + encoded_route = self.encode_path_keys_output(route, output_token) + + input_token: str = _addr_to_str(encoded_route[0].intermediate_currency) # type: ignore[arg-type] + + ether_amount: int = 0 + if input_token == ETH_ADDRESS: + ether_amount = amount_in_max + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_OUT, SETTLE_ALL, TAKE_ALL, TAKE + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + args=[ + v4_actions["SWAP_EXACT_OUT"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + v4_actions["TAKE"], + ], + ) + # SETTING PARAMS + exact_output_params: bytes = encode( + v4_actions_abis["SWAP_EXACT_OUT"], + [ + ( + output_token, + [astuple(path_key) for path_key in encoded_route], + min_hop_price_x_36, + qty, + amount_in_max, + ) + ], + ) + settle_all_params: bytes = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, amount_in_max], + ) + take_all_params: bytes = encode( + v4_actions_abis["TAKE_ALL"], + [output_token, qty], + ) + take_input_params = encode( + v4_actions_abis["TAKE"], [input_token, _addr_to_str(self.address), 0] + ) + + # ENCODING DATA + params = [ + exact_output_params, + settle_all_params, + take_all_params, + take_input_params, + ] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return await self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + async def drop_txn( + self, + address_to: AddressLike, + gas_price: float, + priority_fee: int = 10, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Replaces pending transaction with zero-value ETH transfer + + :param address_to: Zero address or any other valid address to which the zero-value transaction will be sent + + Params `gas_price` and `priority_fee` are Gas Price and Max Priority Fee respectively; + MUST be at least 20% higher than values the original transaction has. + """ + # This one is for legacy transactions + transaction_dict_legacy = { + "nonce": await self.w3.eth.get_transaction_count(self.address) + if custom_nonce is None + else custom_nonce, + "from": _addr_to_str(self.address), + "to": Web3.to_checksum_address(address_to), + "value": Web3.to_wei(0, "wei"), + "gasPrice": Web3.to_wei(gas_price, "gwei"), + "gas": int(self.gas_limit), + "chainId": int(await self.w3.eth.chain_id), + } + signed_txn = self.w3.eth.account.sign_transaction( + transaction_dict_legacy, + self.private_key, + ) + # This one is for post-Merge transactions + transaction_dict = { + "type": 2, + "nonce": await self.w3.eth.get_transaction_count(self.address) + if custom_nonce is None + else custom_nonce, + "from": _addr_to_str(self.address), + "to": _addr_to_str(address_to), + "value": Web3.to_wei(0, "wei"), + "maxFeePerGas": Web3.to_wei(int(gas_price), "gwei"), + "maxPriorityFeePerGas": Web3.to_wei(priority_fee, "gwei"), + "gas": int(self.gas_limit), + "chainId": int(await self.w3.eth.chain_id), + } + signed_txn_london = self.w3.eth.account.sign_transaction( + transaction_dict, + self.private_key, + ) + if self.post_merge: + return await self.w3.eth.send_raw_transaction( + signed_txn_london.raw_transaction + ) + else: + return await self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) + + # Market functions for swapping `qty` amount of `token0` to buy `token1` + async def make_swap_input( + self, + input_token: str, + output_token: str, + qty: int, + qtycap: int, + swap_pool_key: PoolKey | None = None, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param output_token: The token to be bought. + :param qty: The amount of `input_token` to be sold. + :param qtycap: The minimum amount of `output_token` that must be bought for the trade to proceed. + :param swap_pool_key: Optional. The PoolKey for the swap, required if `route` is not provided. + :param hook_data: Optional. The hook data for the swap. This is only used for single hop swaps and will be ignored if `route` is provided. + :param route: Optional. The route for the swap. The first element should be the pool containing `input_token` and the last element should be the pool containing `output_token`. If `route` is provided, it will be used for the swap instead of `swap_pool_key`. + :param custom_nonce: Optional. The nonce for the transaction. + + Make a trade by defining the qty of the input token. + If `route` is provided, it will be used for the swap. Otherwise, `swap_pool_key` must be provided for a single hop swap.""" + result: HexBytes | None = None + if route is None: + if swap_pool_key is None: + raise ValueError("swap_pool_key must be provided for single hop swaps") + result = await self.token_to_token_swap_exact_input( + input_token, + qty, + qtycap, + output_token, + swap_pool_key.fee, + swap_pool_key.tick_spacing, + swap_pool_key.hooks, + hook_data, # type: ignore[arg-type] + min_hop_price_x_36=0, + custom_nonce=custom_nonce, + ) + else: + result = await self.token_to_token_swap_input( + input_token, + qty, + qtycap, + route, + min_hop_price_x_36=[], + custom_nonce=custom_nonce, + ) + return result + + async def make_swap_output( + self, + input_token: str, + output_token: str, + qty: int, + qtycap: int, + swap_pool_key: PoolKey | None = None, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param output_token: The token to be bought. + :param qty: The amount of `output_token` to be bought. + :param qtycap: The maximum amount of `input_token` that can be sold for the trade to proceed. + :param swap_pool_key: Optional. The PoolKey for the swap, required if `route` is not provided. + :param hook_data: Optional. The hook data for the swap. + :param route: Optional. The route for the swap in reverse order. The first element should be the pool containing `output_token` and the last element should be the pool containing `input_token`. + + Make a trade by defining the qty of the output token. + If `route` is provided, it will be used for the swap. Otherwise, `swap_pool_key` must be provided for a single hop swap. + """ + result: HexBytes | None = None + if route is None: + if swap_pool_key is None: + raise ValueError("swap_pool_key must be provided for single hop swaps") + result = await self.token_to_token_swap_exact_output( + input_token, + qty, + qtycap, + output_token, + swap_pool_key.fee, + swap_pool_key.tick_spacing, + swap_pool_key.hooks, + hook_data, # type: ignore[arg-type] + min_hop_price_x_36=0, + custom_nonce=custom_nonce, + ) + else: + result = await self.token_to_token_swap_output( + output_token, + qty, + qtycap, + route, + min_hop_price_x_36=[], + custom_nonce=custom_nonce, + ) + return result + + # Liquidity management functions + async def get_position_info(self, token_id: int) -> dict: + """ + Get information about a liquidity position given its token ID. + :return: A dictionary with the following keys: + + - currency0: The address of the first token in the pool + - currency1: The address of the second token in the pool + - fee: The fee tier of the pool + - tickSpacing: The tick spacing of the pool + - hooks: The hooks address of the pool + - poolID: The truncated pool ID of the position, which is the first 25 bytes of the full pool ID + - tickLower: The lower tick of the position + - tickUpper: The upper tick of the position + - hasSubscriber: A boolean indicating whether the position has a subscriber + - owner: The address of the owner of the position + """ + position_info = await self.position_manager_get_pool_and_position_info(token_id) + + pool_key: PoolKey = position_info["poolKey"] + pool_info = position_info["info"] + pool_info_decoded = self.decode_position_info(pool_info) + owner_of = await self.position_manager_get_owner_of(token_id) + return_value: dict = { + "currency0": pool_key.currency0, + "currency1": pool_key.currency1, + "fee": pool_key.fee, + "tickSpacing": pool_key.tick_spacing, + "hooks": pool_key.hooks, + "poolID": pool_info_decoded["poolID"], + "tickLower": pool_info_decoded["tickLower"], + "tickUpper": pool_info_decoded["tickUpper"], + "hasSubscriber": pool_info_decoded["hasSubscriber"], + "owner": owner_of, + } + return return_value + + async def get_position_value( + self, token_id: int, token0_decimals: int, token1_decimals: int + ) -> dict: + """ + Get the value of a liquidity position given its token ID. + """ + liquidity: int = await self.position_manager_get_position_liquidity(token_id) + position_info = await self.get_position_info(token_id) + slot0 = await self.stateview_get_slot0( + position_info["currency0"], + position_info["currency1"], + position_info["fee"], + position_info["tickSpacing"], + position_info["hooks"], + ) + # price: Decimal = Decimal( + # Decimal( + # decode_sqrt_ratioX96( + # slot0["sqrtPriceX96"], + # ) + # ) + # / Decimal(10 ** (token1_decimals - token0_decimals)) + # ) + sqrt_price = int(slot0["sqrtPriceX96"]) + raw_price: Decimal = (sqrt_price / Decimal(Q96)) ** 2 + decimal_factor: Decimal = 10**token1_decimals / Decimal(10**token0_decimals) + price: Decimal = raw_price / decimal_factor + amounts = self.get_amounts_for_liquidity_by_ticks( + slot0["sqrtPriceX96"], + position_info["tickLower"], + position_info["tickUpper"], + liquidity, + ) + amount0: Decimal = Decimal(amounts["amount0"]) / Decimal(10**token0_decimals) + amount1: Decimal = Decimal(amounts["amount1"]) / Decimal(10**token1_decimals) + return_value: dict = { + "amount0": amounts["amount0"], + "amount1": amounts["amount1"], + # NOTE: unclaimed fees are not yet computed; total values equal principal only + "unclaimed_fees0": 0, + "unclaimed_fees1": 0, + "total_amount0": amounts["amount0"], + "total_amount1": amounts["amount1"], + "value_in_token0": amount0 + amount1 / price, + "value_in_token1": amount1 + amount0 * price, + } + return return_value + + async def create_pool( + self, + pool_key: PoolKey, + sqrt_price_x96: int, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Creates a new liquidity pool without initial liquidity with the specified parameters and a starting price. + """ + function = self.position_manager.functions.initializePool( + astuple(pool_key), + sqrt_price_x96, + ) + tx = await self._build_and_send_tx( + function, + await self._get_tx_params(custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def mint_position( + self, + pool_key: PoolKey, + tick_lower: int, + tick_upper: int, + liquidity: int, + amount0: int, + amount1: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Mints a new liquidity position with the specified parameters. + + :param pool_key: The parameters of the pool for which the position is being minted. + :param tick_lower: The lower tick of the position. + :param tick_upper: The upper tick of the position. + :param liquidity: The amount of liquidity to mint. + :param amount0: The amount of token0 to provide as liquidity. + :param amount1: The amount of token1 to provide as liquidity. + :param recipient: The address that will receive excessive ETH amounts. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the minting. + :return: The transaction hash of the minting transaction. + """ + + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: MINT_POSITION, SETTLE_PAIR, SWEEP, SWEEP + if pool_key.currency0 == ETH_ADDRESS: + ether_amount = amount0 + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + [ + v4_actions["MINT_POSITION"], + v4_actions["SETTLE_PAIR"], + v4_actions["SWEEP"], + v4_actions["SWEEP"], + ], + ) + + # Encoding params + mint_position_params: bytes = encode( + [ + "(address,address,uint24,int24,address)", + "int24", + "int24", + "uint256", + "uint128", + "uint128", + "address", + "bytes", + ], + [ + astuple(pool_key), + tick_lower, + tick_upper, + liquidity, + amount0, + amount1, + recipient, + hook_data, + ], + ) + settle_pair_params: bytes = encode( + ["address", "address"], + [pool_key.currency0, pool_key.currency1], + ) + sweep0_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency0, recipient], + ) + sweep1_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency1, recipient], + ) + params: list[bytes] = [ + mint_position_params, + settle_pair_params, + sweep0_params, + sweep1_params, + ] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = await self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def increase_liquidity( + self, + pool_key: PoolKey, + token_id: int, + amount0_max: int, + amount1_max: int, + liquidity: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Increases the liquidity of an existing position. + + :param pool_key: The parameters of the pool for which the position is being increased. + :param token_id: The token ID of the position to increase liquidity for. + :param amount0_max: The maximum amount of token0 to provide as liquidity. + :param amount1_max: The maximum amount of token1 to provide as liquidity. + :param liquidity: The amount of liquidity to add to the position. + :param recipient: The address that will receive excessive ETH amounts. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the liquidity increase. + :return: The transaction hash of the liquidity increase transaction. + """ + + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: INCREASE_LIQUIDITY, SETTLE_PAIR, SWEEP, SWEEP + if pool_key.currency0 == ETH_ADDRESS: + ether_amount = amount0_max + + actions = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + [ + v4_actions["INCREASE_LIQUIDITY"], + v4_actions["SETTLE_PAIR"], + v4_actions["SWEEP"], + v4_actions["SWEEP"], + ], + ) + # Encoding params + increase_liquidity_params: bytes = encode( + ["uint256", "uint256", "uint128", "uint128", "bytes"], + [ + token_id, + liquidity, + amount0_max, + amount1_max, + hook_data, + ], + ) + settle_pair_params: bytes = encode( + ["address", "address"], + [pool_key.currency0, pool_key.currency1], + ) + sweep0_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency0, recipient], + ) + sweep1_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency1, recipient], + ) + params: list[bytes] = [ + increase_liquidity_params, + settle_pair_params, + sweep0_params, + sweep1_params, + ] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = await self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def decrease_liquidity( + self, + pool_key: PoolKey, + token_id: int, + amount0_min: int, + amount1_min: int, + liquidity: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Decreases the liquidity of an existing position. + + :param pool_key: The parameters of the pool for which the position is being decreased. + :param token_id: The token ID of the position to decrease liquidity for. + :param amount0_min: The minimum amount of token0 to receive from the liquidity decrease. + :param amount1_min: The minimum amount of token1 to receive from the liquidity decrease. + :param liquidity: The amount of liquidity to remove from the position. + :param recipient: The address that will receive the withdrawn liquidity. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the liquidity decrease. + :return: The transaction hash of the liquidity decrease transaction. + """ + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: DECREASE_LIQUIDITY, TAKE_PAIR + actions = encode_packed( + ["uint8", "uint8"], + [ + v4_actions["DECREASE_LIQUIDITY"], + v4_actions["TAKE_PAIR"], + ], + ) + # Encoding params + decrease_liquidity_params: bytes = encode( + ["uint256", "uint256", "uint128", "uint128", "bytes"], + [ + token_id, + liquidity, + amount0_min, + amount1_min, + hook_data, + ], + ) + take_pair_params: bytes = encode( + ["address", "address", "address"], + [pool_key.currency0, pool_key.currency1, recipient], + ) + params: list[bytes] = [decrease_liquidity_params, take_pair_params] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = await self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def collect_fees( + self, + pool_key: PoolKey, + token_id: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Collects the fees accrued by an existing position. + + :param pool_key: The parameters of the pool for which the position is collecting fees. + :param token_id: The token ID of the position for which to collect fees. + :param recipient: The address that will receive the collected fees. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the fee collection. + :return: The transaction hash of the fee collection transaction. + """ + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: DECREASE_LIQUIDITY, TAKE_PAIR + actions = encode_packed( + ["uint8", "uint8"], + [ + v4_actions["DECREASE_LIQUIDITY"], + v4_actions["TAKE_PAIR"], + ], + ) + # Encoding params + decrease_liquidity_params: bytes = encode( + ["uint256", "uint256", "uint128", "uint128", "bytes"], + [ + token_id, + 0, + 0, + 0, + hook_data, + ], + ) + take_pair_params: bytes = encode( + ["address", "address", "address"], + [pool_key.currency0, pool_key.currency1, recipient], + ) + params: list[bytes] = [decrease_liquidity_params, take_pair_params] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = await self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def burn_position( + self, + pool_key: PoolKey, + token_id: int, + amount0_min: int, + amount1_min: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Burns an existing liquidity position. + + :param pool_key: The parameters of the pool for which the position is being burned. + :param token_id: The token ID of the position to burn. + :param amount0_min: The minimum amount of token0 to receive from burning the position. + :param amount1_min: The minimum amount of token1 to receive from burning the position. + :param recipient: The address that will receive the withdrawn liquidity. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the position burn. + :return: The transaction hash of the position burn transaction. + """ + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: BURN_POSITION, TAKE_PAIR + actions = encode_packed( + ["uint8", "uint8"], + [ + v4_actions["BURN_POSITION"], + v4_actions["TAKE_PAIR"], + ], + ) + # Encoding params + burn_position_params: bytes = encode( + ["uint256", "uint128", "uint128", "bytes"], + [ + token_id, + amount0_min, + amount1_min, + hook_data, + ], + ) + take_pair_params: bytes = encode( + ["address", "address", "address"], + [pool_key.currency0, pool_key.currency1, recipient], + ) + params: list[bytes] = [burn_position_params, take_pair_params] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = await self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + async def universal_router_execute( + self, + commands: list[int], + actions: list[list[int]], + params: list[list[list]], + ether_amount: int = 0, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Executes a transaction with the Universal Router with the specified commands, actions, and parameters. + + :param commands: A list of command integers corresponding to the commands to execute. + :param actions: A list of lists of action integers, where each sublist corresponds to the actions for the respective command in the `commands` list. + :param params: A list of lists of lists of parameters, where each sublist corresponds to the parameters for the respective actions in the `actions` list. The structure of the parameters should match the expected input for each action as defined in the Universal Router documentation. + :param ether_amount: The amount of ether to send with the transaction, if any. + :param custom_nonce: Optional. The nonce for the transaction. + + See ``tests/test_uniswap4.py`` for an example which uses this, `test_universal_router_execute_multiaction()` specific test. + + NOTE: This function does not perform checks on the validity of the commands ``SWAP_EXACT_IN*`` and ``SWAP_EXACT_OUT*``. It is the caller's responsibility to ensure that they are correct and will not cause the transaction to revert. + """ + # Validating input parameters + ignore_list = [ + "SWAP_EXACT_IN_SINGLE", + "SWAP_EXACT_IN", + "SWAP_EXACT_OUT_SINGLE", + "SWAP_EXACT_OUT", + ] + if len(commands) != len(actions) or len(actions) != len(params): + raise ValueError("Lists' lengths are not equal.") + for commands_item, actions_item, params_item in zip(commands, actions, params): + command_key: str = self._get_dict_key_by_value( + universal_router_commands, commands_item + ) + if len(actions_item) == 0: + if len(universal_router_commands_abis[command_key]) != sum( + len(sub_list) for sub_list in params_item + ): + raise ValueError("ABI mismatch for " + command_key + " command.") + else: + for specific_action, specific_param in zip(actions_item, params_item): + action_key: str = self._get_dict_key_by_value( + v4_actions, specific_action + ) + if action_key in ignore_list: + continue + if len(v4_actions_abis[action_key]) != len(specific_param): + raise ValueError( + "ABI mismatch for " + + action_key + + " command in " + + command_key + + " command." + ) + + # Encoding data + commands_abi = ["uint8"] * len(commands) + encoded_commands: bytes = encode_packed(commands_abi, commands) + encoded_inputs: list[bytes] = [] + for commands_item, actions_item, params_item in zip(commands, actions, params): + command_key = self._get_dict_key_by_value( + universal_router_commands, commands_item + ) + if len(actions_item) == 0: + encoded_params: bytes = encode( + universal_router_commands_abis[command_key], params_item[0] + ) + encoded_inputs.append(encoded_params) + else: + encoded_actions_with_params = self.encode_actions_with_params( + actions_item, params_item + ) + encoded_params = encode( + universal_router_commands_abis[command_key], + [ + encoded_actions_with_params["actions"], + encoded_actions_with_params["params"], + ], + ) + encoded_inputs.append(encoded_params) + # Execute contract call + result: HexBytes = await self._build_and_send_tx( + self.router.functions.execute( + encoded_commands, encoded_inputs, self._deadline() + ), + await self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + return result + + # Helper functions + def encode_actions_with_params( + self, actions: list[int], params: list[list] + ) -> dict: + encoded_actions_abi = ["uint8"] * len(actions) + encoded_actions: bytes = encode_packed(encoded_actions_abi, actions) + encoded_params: list[bytes] = [] + for actions_item, params_item in zip(actions, params): + action_key = self._get_dict_key_by_value(v4_actions, actions_item) + encoded_params_item: bytes = encode( + v4_actions_abis[action_key], params_item + ) + encoded_params.append(encoded_params_item) + return_value: dict = { + "actions": encoded_actions, + "params": encoded_params, + } + return return_value + + @staticmethod + def _get_dict_key_by_value(param_dict: dict, value: int) -> str: + return_value = next((k for k, v in param_dict.items() if v == value), None) + if return_value is None: + raise IndexError("Key is not found.") + return str(return_value) + + @staticmethod + def get_liquidity_for_amount0( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, amount0: int + ) -> int: + """ + Helper function to calculate the amount of liquidity that can be provided for a given amount of `token0` and price range defined by `sqrt_ ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + liquidity: int = (amount0 * (sqrt_ratio_a_x96 * sqrt_ratio_b_x96 // Q96)) // ( + sqrt_ratio_b_x96 - sqrt_ratio_a_x96 + ) + return liquidity + + @staticmethod + def get_liquidity_for_amount1( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, amount1: int + ) -> int: + """ + Helper function to calculate the amount of liquidity that can be provided for a given amount of `token1` and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + liquidity: int = (amount1 * Q96) // (sqrt_ratio_b_x96 - sqrt_ratio_a_x96) + return liquidity + + def get_liquidity_for_amounts( + self, + sqrt_ratio_a_x96: int, + sqrt_ratio_b_x96: int, + sqrt_ratio_current_x96: int, + amount0: int, + amount1: int, + ) -> int: + """ + Helper function to calculate the amount of liquidity that can be provided for given amounts of `token0` and `token1` and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + if sqrt_ratio_current_x96 <= sqrt_ratio_a_x96: + liquidity: int = self.get_liquidity_for_amount0( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, amount0 + ) + elif sqrt_ratio_current_x96 < sqrt_ratio_b_x96: + liquidity0: int = self.get_liquidity_for_amount0( + sqrt_ratio_current_x96, sqrt_ratio_b_x96, amount0 + ) + liquidity1: int = self.get_liquidity_for_amount1( + sqrt_ratio_a_x96, sqrt_ratio_current_x96, amount1 + ) + liquidity = min(liquidity0, liquidity1) + else: + liquidity = self.get_liquidity_for_amount1( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, amount1 + ) + return liquidity + + @staticmethod + def get_amount0_for_liquidity( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, liquidity: int + ) -> int: + """ + Helper function to calculate the amount of `token0` that can be provided for a given amount of liquidity and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + amount0: int = ( + (liquidity * Q96 * (sqrt_ratio_b_x96 - sqrt_ratio_a_x96)) + // sqrt_ratio_b_x96 + // sqrt_ratio_a_x96 + ) + return amount0 + + @staticmethod + def get_amount1_for_liquidity( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, liquidity: int + ) -> int: + """ + Helper function to calculate the amount of `token1` that can be provided for a given amount of liquidity and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + amount1: int = (liquidity * (sqrt_ratio_b_x96 - sqrt_ratio_a_x96)) // Q96 + return amount1 + + def get_amounts_for_liquidity( + self, + sqrt_ratio_a_x96: int, + sqrt_ratio_b_x96: int, + sqrt_ratio_current_x96: int, + liquidity: int, + ) -> dict: + """ + Helper function to calculate the amounts of `token0` and `token1` that can be provided for a given amount of liquidity and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + amount0: int = 0 + amount1: int = 0 + if sqrt_ratio_current_x96 <= sqrt_ratio_a_x96: + amount0 = self.get_amount0_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, liquidity + ) + elif sqrt_ratio_current_x96 < sqrt_ratio_b_x96: + amount0 = self.get_amount0_for_liquidity( + sqrt_ratio_current_x96, sqrt_ratio_b_x96, liquidity + ) + amount1 = self.get_amount1_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_current_x96, liquidity + ) + else: + amount1 = self.get_amount1_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, liquidity + ) + return_value: dict = { + "amount0": amount0, + "amount1": amount1, + } + return return_value + + def get_amounts_for_liquidity_by_ticks( + self, ratio_current_x96: int, tick_lower: int, tick_upper: int, liquidity: int + ) -> dict: + sqrt_ratio_a_x96 = get_sqrt_ratio_at_tick(tick_lower) + sqrt_ratio_b_x96 = get_sqrt_ratio_at_tick(tick_upper) + return_value: dict = self.get_amounts_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, ratio_current_x96, liquidity + ) + return return_value + + def get_liquidity_for_amounts_by_ticks( + self, + ratio_current_x96: int, + tick_lower: int, + tick_upper: int, + amount0: int, + amount1: int, + ) -> int: + sqrt_ratio_a_x96 = get_sqrt_ratio_at_tick(tick_lower) + sqrt_ratio_b_x96 = get_sqrt_ratio_at_tick(tick_upper) + liquidity = self.get_liquidity_for_amounts( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, ratio_current_x96, amount0, amount1 + ) + return liquidity + + async def get_minted_token_id(self, tx_hash: str) -> list[int]: + """ + Helper function to extract the token ID of a newly minted position from the transaction receipt of the minting transaction. + + :return: A list of token IDs of the newly minted positions; empty list if none can be extracted. In most cases, this list will contain only one token ID, but in some cases (e.g., if multiple positions are minted in a single transaction), it may contain multiple token IDs. + """ + transaction_receipt = await self.w3.eth.get_transaction_receipt(tx_hash) # type: ignore [arg-type] + logs = self.position_manager.events.Transfer().process_receipt( + transaction_receipt + ) + return_value: list[int] = [] + for log in logs: + try: + if _addr_to_str(log.args["from"]) == ZERO_HOOK: + minted_token_id: int = log.args.id + return_value.append(minted_token_id) + except (AttributeError, KeyError): + logger.warning( + "Could not extract minted token ID from transaction receipt for transaction hash: %s.", + tx_hash, + ) + return return_value + + @staticmethod + def decode_position_info(position_info: int) -> dict: + """ + + :return: + A dictionary with the following keys: + - `tickLower`: The lower tick of the position. + - `tickUpper`: The upper tick of the position. + - `poolID`: The truncated pool ID of the position, which is the first 25 bytes of the full pool ID. + - `hasSubscriber`: A boolean indicating whether the position has a subscriber. + """ + tick_lower_offset = 8 + tick_upper_offset = 32 + pool_id_offset = 56 + sign_bit = 0x800000 + + # The hasSubscriber flag is stored in the least significant byte of the position info, so we can use a bitwise AND operation with a mask of 0xFF to extract it. + mask = 0xFF + has_subscriber: bool = (position_info & mask) != 0 + + # The tick lower and tick upper values are stored in the next 3 bytes each, so we shift the position info to the right by the respective offsets and use a bitwise AND operation with a mask of 0xFFFFFF to extract them. We also check if the sign bit is set to determine if the tick values are negative, and if so, we subtract 0x1000000 from them to get the correct negative value. + mask = 0xFFFFFF + tick_lower: int = (position_info >> tick_lower_offset) & mask + if tick_lower & sign_bit: + tick_lower = tick_lower - 0x1000000 + + tick_upper: int = (position_info >> tick_upper_offset) & mask + if tick_upper & sign_bit: + tick_upper = tick_upper - 0x1000000 + + # The pool ID is stored in the remaining bytes, so we shift the position info to the right by the pool ID offset to get the pool ID. + rest_part: int = position_info >> pool_id_offset + pool_id: bytes = rest_part.to_bytes(25, byteorder="big") + + return_value: dict = { + "tickLower": tick_lower, + "tickUpper": tick_upper, + "poolID": pool_id, + "hasSubscriber": has_subscriber, + } + return return_value + + @staticmethod + def encode_path_keys_input( + path: list[PoolKey], + currency_in: str, + hook_data_list: list[bytes] | None = None, + ) -> list[PathKey]: + """ + Encodes a list of PoolKeys into the format expected by the quoter for multi-hop ExactInput quotes. + """ + encoded_path: list[PathKey] = [] + if hook_data_list is None: + hook_data_list = [b""] * len(path) + else: + if len(hook_data_list) != len(path): + raise ValueError("Length of hook_data_list must match length of path") + for pool_key, hook_data in zip(path, hook_data_list): + currency_out: str = ( + pool_key.currency1 + if currency_in.lower() == pool_key.currency0.lower() + else pool_key.currency0 + ) + path_key: PathKey = PathKey( + currency_out, + pool_key.fee, + pool_key.tick_spacing, + pool_key.hooks, + hook_data, + ) + encoded_path.append(path_key) + currency_in = currency_out + return encoded_path + + @staticmethod + def encode_path_keys_output( + path: list[PoolKey], + currency_out: str, + hook_data_list: list[bytes] | None = None, + ) -> list[PathKey]: + """ + Encodes a list of PoolKeys into the format expected by the quoter for multi-hop ExactOutput quotes. + + """ + encoded_path: list[PathKey] = [] + if hook_data_list is None: + hook_data_list = [b""] * len(path) + else: + if len(hook_data_list) != len(path): + raise ValueError("Length of hook_data_list must match length of path") + for pool_key, hook_data in zip(reversed(path), reversed(hook_data_list)): + currency_in: str = ( + pool_key.currency1 + if currency_out.lower() == pool_key.currency0.lower() + else pool_key.currency0 + ) + path_key: PathKey = PathKey( + currency_in, + pool_key.fee, + pool_key.tick_spacing, + pool_key.hooks, + hook_data, + ) + encoded_path.insert(0, path_key) + currency_out = currency_in + return encoded_path + + @staticmethod + def get_pool_id(pool: PoolKey) -> HexBytes: + """Computes the pool ID for a given PoolKey by hashing its parameters.""" + pool_data = eth_abi.abi.encode( + types=["address", "address", "uint24", "int24", "address"], + args=[ + pool.currency0, + pool.currency1, + pool.fee, + pool.tick_spacing, + pool.hooks, + ], + ) + pool_id = Web3.keccak(pool_data) + return pool_id + + async def get_token( + self, address: AddressLike, abi_name: str = "erc20" + ) -> ERC20Token: + """ + Retrieves metadata from the ERC20 contract of a given token, like its name, symbol, and decimals. + """ + if address == ETH_ADDRESS or address == _str_to_addr(ETH_ADDRESS): + return ERC20Token( + address=address, + name="ETH", + symbol="ETH", + decimals=18, + ) + token_contract = _load_contract(self.w3, abi_name, address=address) + try: + _name = await token_contract.functions.name().call() + _symbol = await token_contract.functions.symbol().call() + decimals = await token_contract.functions.decimals().call() + except ValueError: + raise InvalidToken(address) + try: + name = _name.decode() + except (ValueError, AttributeError): + name = str(_name) + try: + symbol = _symbol.decode() + except (ValueError, AttributeError) as e: + logger.warning( + "Error occurred while decoding symbol for %s: %s", + _addr_to_str(address), + e, + ) + symbol = str(_symbol) + return ERC20Token(symbol, address, name, decimals) + + async def get_token_balance(self, erc20: AddressLike) -> Decimal: + """Get the balance of an ERC20 token for your address.""" + contract = _load_contract(self.w3, abi_name="erc20", address=erc20) + decimals: int = await contract.functions.decimals().call() + balance: int = await contract.functions.balanceOf(self.address).call() + return_balance: Decimal = Decimal(balance) / Decimal(10**decimals) + return return_balance + + async def get_balance(self) -> Decimal: + """Get the balance of ETH for your address.""" + balance: int = await self.w3.eth.get_balance(self.address) + return_balance: Decimal = Decimal(balance) / Decimal(10**18) + return return_balance + + def load_contract_with_abi( + self, abi_name: str, address: AddressLike + ) -> AsyncContract: + return self.w3.eth.contract(address=address, abi=_load_abi(abi_name)) + + def erc20_contract(self, token_addr: AddressLike) -> AsyncContract: + return self.load_contract_with_abi(abi_name="erc20", address=token_addr) + + def _deadline(self) -> int: + """Get a predefined deadline. 10min by default.""" + return int(time.time()) + 10 * 60 + + async def _get_tx_params( + self, value: int = 0, custom_nonce: Nonce | None = None + ) -> TxParams: + """Get generic transaction parameters.""" + if custom_nonce is not None and custom_nonce < Nonce(0): + raise ValueError("Nonce can only be a positive integer.") + if not self.post_merge: + return { + "from": _addr_to_str(self.address), + "value": Wei(value), + "gas": int(self.gas_limit), + "gasPrice": Web3.to_wei(self.gas_price, "gwei"), + "nonce": Nonce(max(self.last_nonce, 0)) + if custom_nonce is None + else custom_nonce, + } + else: + return { + "from": _addr_to_str(self.address), + "gas": int(self.gas_limit), + "maxPriorityFeePerGas": Web3.to_wei(self.priority_fee, "gwei"), + "maxFeePerGas": Web3.to_wei(self.gas_price, "gwei"), + "type": 2, + "chainId": await self.w3.eth.chain_id, + "value": Wei(value), + "nonce": Nonce(max(self.last_nonce, 0)) + if custom_nonce is None + else custom_nonce, + } + + async def _build_and_send_tx( + self, + function: AsyncContractFunction, + tx_params: TxParams | None = None, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """Build and send a transaction.""" + async with self._nonce_lock: + if not tx_params: + tx_params = await self._get_tx_params(custom_nonce=custom_nonce) + # Reserve the nonce before yielding so concurrent calls never share it. + if custom_nonce is None: + self.last_nonce = Nonce(tx_params["nonce"] + 1) + transaction = await function.build_transaction(tx_params) + signed_txn = self.w3.eth.account.sign_transaction( + transaction, private_key=self.private_key + ) + return await self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) + + +def _load_contract(w3: AsyncWeb3, abi_name: str, address: AddressLike) -> AsyncContract: + address = w3.to_checksum_address(address) + return w3.eth.contract(address=address, abi=_load_abi(abi_name)) + + +def _load_contract_erc20(w3: AsyncWeb3, address: AddressLike) -> AsyncContract: + return _load_contract(w3, "erc20", address) diff --git a/uniswap/cli.py b/uniswap/cli.py new file mode 100644 index 00000000..8ba54e2f --- /dev/null +++ b/uniswap/cli.py @@ -0,0 +1,118 @@ +import logging +import os + +import click +from dotenv import load_dotenv +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 + +logger = logging.getLogger(__name__) + +# Global used in _coerce_to_checksum to look up tokens +_uni: Uniswap | None = None + + +def _coerce_to_checksum(addr: str) -> str: + assert _uni + tokens = get_tokens(_uni.netname) + if not addr.startswith("0x"): + if addr.upper() in tokens: + return tokens[addr.upper()] + else: + raise ValueError( + "token was not an address, and a shorthand was not found in the token db" + ) + if Web3.is_checksum_address(addr): + return addr + else: + return Web3.to_checksum_address(addr) + + +@click.group() +@click.option("-v", "--verbose", is_flag=True) +@click.option( + "--version", + type=click.Choice(["1", "2", "3"]), + default=os.getenv("UNISWAP_VERSION", "3"), +) +@click.pass_context +def main(ctx: click.Context, verbose: bool, version: str) -> None: + logging.basicConfig(level=logging.INFO if verbose else logging.WARNING) + load_dotenv() + + ctx.ensure_object(dict) + ctx.obj["VERBOSE"] = verbose + ctx.obj["UNISWAP"] = Uniswap(None, None, version=int(version)) + global _uni + _uni = ctx.obj["UNISWAP"] + + +@main.command() +@click.argument("token_in", type=_coerce_to_checksum) +@click.argument("token_out", type=_coerce_to_checksum) +@click.option( + "--raw", + is_flag=True, + help="Don't normalize the quoted price to the output token's decimals", +) +@click.option( + "--quantity", + help="Quantity of output tokens to get price of. Falls back to one full unit of the input token by default (10**18 for WETH, for example).", +) +@click.pass_context +def price( + ctx: click.Context, + token_in: AddressLike, + token_out: AddressLike, + raw: bool, + quantity: int | None = None, +) -> None: + """Returns the price of ``quantity`` tokens of ``token_in`` quoted in ``token_out``.""" + uni: Uniswap = ctx.obj["UNISWAP"] + if quantity is None: + if token_in == ETH_ADDRESS: + decimals = 18 + else: + decimals = uni.get_token(token_in).decimals + quantity = 10**decimals + price = uni.get_price_input( + token_in, token_out, qty=quantity, fee=FeeTier.TIER_3000 + ) + if raw: + click.echo(price) + else: + if token_in == ETH_ADDRESS: + decimals = 18 + else: + decimals = uni.get_token(token_out).decimals + click.echo(price / 10**decimals) + + +@main.command() +@click.argument("token", type=_coerce_to_checksum) +@click.pass_context +def token(ctx: click.Context, token: AddressLike) -> None: + """Show metadata for token""" + uni: Uniswap = ctx.obj["UNISWAP"] + t1 = uni.get_token(token) + click.echo(t1) + + +@main.command() +@click.option("--metadata", is_flag=True, help="Also get metadata for tokens") +@click.pass_context +def tokendb(ctx: click.Context, metadata: bool) -> None: + """List known token addresses""" + uni: Uniswap = ctx.obj["UNISWAP"] + for symbol, addr in get_tokens(uni.netname).items(): + if metadata and addr != "0x0000000000000000000000000000000000000000": + data = uni.get_token(_str_to_addr(addr)) + assert data.symbol.lower() == symbol.lower() + click.echo(data) + else: + click.echo(BaseToken(symbol, addr)) diff --git a/uniswap/constants.py b/uniswap/constants.py new file mode 100644 index 00000000..f7d15db0 --- /dev/null +++ b/uniswap/constants.py @@ -0,0 +1,565 @@ +from typing import cast + +from web3.types import RPCEndpoint + +# look at web3/middleware/cache.py for reference +# RPC methods that will be cached inside _get_eth_simple_cache_middleware +SIMPLE_CACHE_RPC_WHITELIST = cast( + set[RPCEndpoint], + { + "eth_chainId", + }, +) + +ETH_ADDRESS = "0x0000000000000000000000000000000000000000" +WETH9_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" +ZERO_HOOK = "0x0000000000000000000000000000000000000000" + +Q96 = 2**96 + + +# see: https://chainid.network/chains/ +_netid_to_name = { + 1: "mainnet", + 3: "ropsten", + 4: "rinkeby", + 5: "görli", + 10: "optimism", + 42: "kovan", + 56: "binance", + 97: "binance_testnet", + 100: "xdai", + 130: "unichain", + 137: "polygon", + 143: "monad", + 196: "xlayer", + 250: "fantom", + 480: "worldchain", + 1868: "soneium", + 4217: "tempo", + 4326: "megaeth", + 4663: "robinhood", + 8453: "base", + 42161: "arbitrum", + 42220: "celo", + 43114: "avalanche", + 57073: "ink", + 421611: "arbitrum_testnet", + 7777777: "zora", + 1666600000: "harmony_mainnet", + 1666700000: "harmony_testnet", + 11155111: "sepolia", +} + +_factory_contract_addresses_v1 = { + "mainnet": "0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95", + "ropsten": "0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351", + "rinkeby": "0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36", + "kovan": "0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30", + "görli": "0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA", +} + + +# For v2 the address is the same on mainnet, Ropsten, Rinkeby, Görli, and Kovan +# https://uniswap.org/docs/v2/smart-contracts/factory +_factory_contract_addresses_v2 = { + "mainnet": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "ropsten": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "rinkeby": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "görli": "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f", + "xdai": "0xA818b4F111Ccac7AA31D0BCc0806d64F2E0737D7", + "binance": "0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73", + "binance_testnet": "0x6725F303b657a9451d8BA641348b6761A6CC7a17", + # SushiSwap on Harmony + "harmony_mainnet": "0xc35DADB65012eC5796536bD9864eD8773aBc74C4", + "harmony_testnet": "0xc35DADB65012eC5796536bD9864eD8773aBc74C4", + "sepolia": "0x7E0987E5b3a30e3f2828572Bb659A548460a3003", +} + +_router_contract_addresses_v2 = { + "mainnet": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D", + "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", +} + +MAX_UINT_128 = (2**128) - 1 + +# Source: https://github.com/Uniswap/v3-core/blob/v1.0.0/contracts/libraries/TickMath.sol#L8-L11 +MIN_TICK = -887272 +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} + +# 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), +} +# Source: +# https://developers.uniswap.org/docs/protocols/v4/deployments +_router_contract_addresses_v4 = { + # Production addresses + "mainnet": "0x4c82d1fbfe28c977cbb58d8c7ff8fcf9f70a2cca", + "unichain": "0xfdf682f51fe81aa4898f0ae2163d8a55c127fbc7", + "optimism": "0x8b844f885672f333bc0042cb669255f93a4c1e6b", + "base": "0xfdf682f51fe81aa4898f0ae2163d8a55c127fbc7", + "arbitrum": "0x8b844f885672f333bc0042cb669255f93a4c1e6b", + "polygon": "0x8b844f885672f333bc0042cb669255f93a4c1e6b", + "zora": "0xfdf682f51fe81aa4898f0ae2163d8a55c127fbc7", + "worldchain": "0x8b844f885672f333bc0042cb669255f93a4c1e6b", + "ink": "0x112908dac86e20e7241b0927479ea3bf935d1fa0", + "soneium": "0x8b844f885672f333bc0042cb669255f93a4c1e6b", + "avalanche": "0x8b844f885672f333bc0042cb669255f93a4c1e6b", + "binance": "0x8b844f885672f333bc0042cb669255f93a4c1e6b", + "celo": "0x8b844f885672f333bc0042cb669255f93a4c1e6b", + "monad": "0xfdf682f51fe81aa4898f0ae2163d8a55c127fbc7", + "megaeth": "0x47837eb80db5908eabba9105626d9b348bea7b02", + "xlayer": "0x8b844f885672f333bc0042cb669255f93a4c1e6b", + "tempo": "0xa2dc7d0266f0cc50b3eeaf36c9bfcecff1beea91", + "robinhood": "0x8876789976decbfcbbbe364623c63652db8c0904", +} + +_quoter_contract_addresses_v4 = { + # Production addresses + "mainnet": "0x52f0e24d1c21c8a0cb1e5a5dd6198556bd9e1203", + "unichain": "0x333e3c607b141b18ff6de9f258db6e77fe7491e0", + "optimism": "0x1f3131a13296fb91c90870043742c3cdbff1a8d7", + "base": "0x0d5e0f971ed27fbff6c2837bf31316121532048d", + "arbitrum": "0x3972c00f7ed4885e145823eb7c655375d275a1c5", + "polygon": "0xb3d5c3dfc3a7aebff71895a7191796bffc2c81b9", + "zora": "0x5edaccc0660e0a2c44b06e07ce8b915e625dc2c6", + "worldchain": "0x55d235b3ff2daf7c3ede0defc9521f1d6fe6c5c0", + "ink": "0x3972c00f7ed4885e145823eb7c655375d275a1c5", + "soneium": "0x3972c00f7ed4885e145823eb7c655375d275a1c5", + "avalanche": "0xbe40675bb704506a3c2ccfb762dcfd1e979845c2", + "binance": "0x9f75dd27d6664c475b90e105573e550ff69437b0", + "celo": "0x28566da1093609182dff2cb2a91cfd72e61d66cd", + "monad": "0xa222dd357a9076d1091ed6aa2e16c9742dd26891", + "megaeth": "0x94bdc671f0c35f44a1daa53143fd1f868d1623b9", + "xlayer": "0x8928074ca1b241d8ec02815881c1af11e8bc5219", + "tempo": "0x20e6487c371a2086f841ef453f85378223df4f4e", + "robinhood": "0x8dc178efb8111bb0973dd9d722ebeff267c98f94", +} + +_stateview_contract_addresses_v4 = { + # Production addresses + "mainnet": "0x7ffe42c4a5deea5b0fec41c94c136cf115597227", + "unichain": "0x86e8631a016f9068c3f085faf484ee3f5fdee8f2", + "optimism": "0xc18a3169788f4f75a170290584eca6395c75ecdb", + "base": "0xa3c0c9b65bad0b08107aa264b0f3db444b867a71", + "arbitrum": "0x76fd297e2d437cd7f76d50f01afe6160f86e9990", + "polygon": "0x5ea1bd7974c8a611cbab0bdcafcb1d9cc9b3ba5a", + "zora": "0x385785af07d63b50d0a0ea57c4ff89d06adf7328", + "worldchain": "0x51d394718bc09297262e368c1a481217fdeb71eb", + "ink": "0x76fd297e2d437cd7f76d50f01afe6160f86e9990", + "soneium": "0x76fd297e2d437cd7f76d50f01afe6160f86e9990", + "avalanche": "0xc3c9e198c735a4b97e3e683f391ccbdd60b69286", + "binance": "0xd13dd3d6e93f276fafc9db9e6bb47c1180aee0c4", + "celo": "0xbc21f8720babf4b20d195ee5c6e99c52b76f2bfb", + "monad": "0x77395f3b2e73ae90843717371294fa97cc419d64", + "megaeth": "0x726f84e1dfb8d375a365e0808282f40d52d3e4e8", + "xlayer": "0x76fd297e2d437cd7f76d50f01afe6160f86e9990", + "tempo": "0x21b954fba3f5ddebe77ef2d47a3100c066908b2a", + "robinhood": "0xf3334192d15450cdd385c8b70e03f9a6bd9e673b", +} + +_permit2_contract_addresses_v4 = { + # Production addresses + "mainnet": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "unichain": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "optimism": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "base": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "arbitrum": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "polygon": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "zora": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "worldchain": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "ink": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "soneium": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "avalanche": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "binance": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "celo": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "monad": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "megaeth": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "xlayer": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "tempo": "0x000000000022D473030F116dDEE9F6B43aC78BA3", + "robinhood": "0x000000000022D473030F116dDEE9F6B43aC78BA3", +} + +_poolmanager_contract_addresses_v4 = { + # Production addresses + "mainnet": "0x000000000004444c5dc75cB358380D2e3dE08A90", + "unichain": "0x1f98400000000000000000000000000000000004", + "optimism": "0x9a13f98cb987694c9f086b1f5eb990eea8264ec3", + "base": "0x498581ff718922c3f8e6a244956af099b2652b2b", + "arbitrum": "0x360e68faccca8ca495c1b759fd9eee466db9fb32", + "polygon": "0x67366782805870060151383f4bbff9dab53e5cd6", + "zora": "0x0575338e4c17006ae181b47900a84404247ca30f", + "worldchain": "0xb1860d529182ac3bc1f51fa2abd56662b7d13f33", + "ink": "0x360e68faccca8ca495c1b759fd9eee466db9fb32", + "soneium": "0x360e68faccca8ca495c1b759fd9eee466db9fb32", + "avalanche": "0x06380c0e0912312b5150364b9dc4542ba0dbbc85", + "binance": "0x28e2ea090877bf75740558f6bfb36a5ffee9e9df", + "celo": "0x288dc841A52FCA2707c6947B3A777c5E56cd87BC", + "monad": "0x188d586ddcf52439676ca21a244753fa19f9ea8e", + "megaeth": "0xacb7e78fa05d562e0a5d3089ec896d57d057d38e", + "xlayer": "0x360e68faccca8ca495c1b759fd9eee466db9fb32", + "tempo": "0x33620f62c5b9b2086dd6b62f4a297a9f30347029", + "robinhood": "0x8366a39cc670b4001a1121b8f6a443a643e40951", +} + +_position_descriptor_contract_addresses_v4 = { + # Production addresses + "mainnet": "0xd1428ba554f4c8450b763a0b2040a4935c63f06c", + "unichain": "0x9fb28449a191cd8c03a1b7abfb0f5996ecf7f722", + "optimism": "0xedd81496169c46df161b8513a52ffecaaaa66743", + "base": "0x25d093633990dc94bedeed76c8f3cdaa75f3e7d5", + "arbitrum": "0xe2023f3fa515cf070e07fd9d51c1d236e07843f4", + "polygon": "0x0892771f0c1b78ad6013d6e5536007e1c16e6794", + "zora": "0x7d64630bbb4993b5578dbd65e400961c9e68d55a", + "worldchain": "0x7da419153bd420b689f312363756d76836aeace4", + "ink": "0x42e3ccd9b7f67b5b2ee0c12074b84ccf2a8e7f36", + "soneium": "0x42e3ccd9b7f67b5b2ee0c12074b84ccf2a8e7f36", + "avalanche": "0x2b1aed9445b05ac1a3b203eccc1e25dd9351f0a9", + "binance": "0xf0432f360703ec3d33931a8356a75a77d8d380e1", + "celo": "0x5727E22b25fEEe05E8dFa83C752B86F19D102D8A", + "monad": "0x5770d2914355a6d0a39a70aeea9bcce55df4201b", + "megaeth": "0xa9fdbb9d3dce2e1cfb91c4af1b8cf4ed62c0041a", + "xlayer": "0x9e9fbbef0e1bd752e83de5acff3d0c936a9e5a4b", + "tempo": "0xc73ed2cba8c347593d1d9120aa0be8ff88c6ff77", + "robinhood": "0x9639443158e8c5efa35bd45287bf2effd3d8dc06", +} + +_position_manager_contract_addresses_v4 = { + # Production addresses + "mainnet": "0xbd216513d74c8cf14cf4747e6aaa6420ff64ee9e", + "unichain": "0x4529a01c7a0410167c5740c487a8de60232617bf", + "optimism": "0x3c3ea4b57a46241e54610e5f022e5c45859a1017", + "base": "0x7c5f5a4bbd8fd63184577525326123b519429bdc", + "arbitrum": "0xd88f38f930b7952f2db2432cb002e7abbf3dd869", + "polygon": "0x1ec2ebf4f37e7363fdfe3551602425af0b3ceef9", + "zora": "0xf66c7b99e2040f0d9b326b3b7c152e9663543d63", + "worldchain": "0xc585e0f504613b5fbf874f21af14c65260fb41fa", + "ink": "0x1b35d13a2e2528f192637f14b05f0dc0e7deb566", + "soneium": "0x1b35d13a2e2528f192637f14b05f0dc0e7deb566", + "avalanche": "0xb74b1f14d2754acfcbbe1a221023a5cf50ab8acd", + "binance": "0x7a4a5c919ae2541aed11041a1aeee68f1287f95b", + "celo": "0xf7965f3981e4d5bc383bfbcb61501763e9068ca9", + "monad": "0x5b7ec4a94ff9bedb700fb82ab09d5846972f4016", + "megaeth": "0x9ae0921e981aaa7308f176f8d4f9129b9247c89d", + "xlayer": "0xcf1eafc6928dc385a342e7c6491d371d2871458b", + "tempo": "0x3fc79444f8eacc1894775493ff3fa41f1e35ce11", + "robinhood": "0x58daec3116aae6d93017baaea7749052e8a04fa7", +} + +_reserves_lens_contract_addresses_v4 = { + # Production addresses + "mainnet": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "unichain": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "optimism": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "base": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "arbitrum": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "polygon": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "zora": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "worldchain": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "ink": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "soneium": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "avalanche": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "binance": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "celo": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "monad": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "megaeth": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "xlayer": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "tempo": "0x0000001b173C3bbF3984D417d8614E3eed34865B", + "robinhood": "0x0000001b173C3bbF3984D417d8614E3eed34865B", +} + + +# Source: https://github.com/Uniswap/universal-router/blob/main/contracts/libraries/Commands.sol +universal_router_commands = { + "V3_SWAP_EXACT_IN": 0x00, + "V3_SWAP_EXACT_OUT": 0x01, + "PERMIT2_TRANSFER_FROM": 0x02, + "PERMIT2_PERMIT_BATCH": 0x03, + "SWEEP": 0x04, + "TRANSFER": 0x05, + "PAY_PORTION": 0x06, + "PAY_PORTION_FULL_PRECISION": 0x07, # Deprecated + "V2_SWAP_EXACT_IN": 0x08, + "V2_SWAP_EXACT_OUT": 0x09, + "PERMIT2_PERMIT": 0x0A, + "WRAP_ETH": 0x0B, + "UNWRAP_WETH": 0x0C, + "PERMIT2_TRANSFER_FROM_BATCH": 0x0D, + "BALANCE_CHECK_ERC20": 0x0E, + "V4_SWAP": 0x10, + "V3_POSITION_MANAGER_PERMIT": 0x11, + "V3_POSITION_MANAGER_CALL": 0x12, + "V4_INITIALIZE_POOL": 0x13, + "V4_POSITION_MANAGER_CALL": 0x14, + "EXECUTE_SUB_PLAN": 0x21, +} +# Source: https://github.com/Uniswap/universal-router/blob/d3123d99493aa6524bc5de44423c3961177758cb/test/integration-tests/shared/planner.ts +universal_router_commands_abis = { + "V3_SWAP_EXACT_IN": [ + "address", + "uint256", + "uint256", + "bytes", + "bool", + ], + "V3_SWAP_EXACT_OUT": [ + "address", + "uint256", + "uint256", + "bytes", + "bool", + ], + "PERMIT2_TRANSFER_FROM": [ + "address", + "address", + "uint256", + ], + "PERMIT2_PERMIT_BATCH": [ + "((address,uint160,uint48,uint48)[],address,uint256)", + "bytes", + ], + "SWEEP": [ + "address", + "address", + "uint256", + ], + "TRANSFER": [ + "address", + "address", + "uint256", + ], + "PAY_PORTION": [ + "address", + "address", + "uint256", + ], + "PAY_PORTION_FULL_PRECISION": [ + "address", + "address", + "uint256", + ], + "V2_SWAP_EXACT_IN": [ + "address", + "uint256", + "uint256", + "address[]", + "bool", + ], + "V2_SWAP_EXACT_OUT": [ + "address", + "uint256", + "uint256", + "address[]", + "bool", + ], + "PERMIT2_PERMIT": [ + "((address,uint160,uint48,uint48),address,uint256)", + "bytes", + ], + "WRAP_ETH": [ + "address", + "uint256", + ], + "UNWRAP_WETH": [ + "address", + "uint256", + ], + "PERMIT2_TRANSFER_FROM_BATCH": [ + "(address,uint256,address,bytes)[]", + ], + "BALANCE_CHECK_ERC20": [ + "address", + "address", + "uint256", + ], + "V4_SWAP": [ + "bytes", + "bytes[]", + ], + "V3_POSITION_MANAGER_PERMIT": [ + "bytes", + ], + "V3_POSITION_MANAGER_CALL": [ + "bytes", + ], + "V4_INITIALIZE_POOL": [ + "(address,address,uint24,int24,address)", + "uint160", + ], + "V4_POSITION_MANAGER_CALL": [ + "bytes", + ], + "EXECUTE_SUB_PLAN": [ + "bytes", + "bytes[]", + ], +} + +# Source: https://github.com/Uniswap/v4-periphery/blob/main/src/libraries/Actions.sol +# NOTE: `INCREASE_LIQUIDITY_FROM_DELTAS` and `MINT_POSITION_FROM_DELTAS` are deprecated and should not be used, see comments in the source file. +# NOTE: `DONATE` is not supported in the position manager or router. +v4_actions = { + "INCREASE_LIQUIDITY": 0x00, + "DECREASE_LIQUIDITY": 0x01, + "MINT_POSITION": 0x02, + "BURN_POSITION": 0x03, + "INCREASE_LIQUIDITY_FROM_DELTAS": 0x04, + "MINT_POSITION_FROM_DELTAS": 0x05, + "SWAP_EXACT_IN_SINGLE": 0x06, + "SWAP_EXACT_IN": 0x07, + "SWAP_EXACT_OUT_SINGLE": 0x08, + "SWAP_EXACT_OUT": 0x09, + "DONATE": 0x0A, + "SETTLE": 0x0B, + "SETTLE_ALL": 0x0C, + "SETTLE_PAIR": 0x0D, + "TAKE": 0x0E, + "TAKE_ALL": 0x0F, + "TAKE_PORTION": 0x10, + "TAKE_PAIR": 0x11, + "CLOSE_CURRENCY": 0x12, + "CLEAR_OR_TAKE": 0x13, + "SWEEP": 0x14, + "WRAP": 0x15, + "UNWRAP": 0x16, + "MINT_6909": 0x17, + "BURN_6909": 0x18, +} + +# Source: https://github.com/Uniswap/universal-router/blob/d3123d99493aa6524bc5de44423c3961177758cb/test/integration-tests/shared/v4Planner.ts +v4_actions_abis = { + "INCREASE_LIQUIDITY": [ + "uint256", + "uint256", + "uint128", + "uint128", + "bytes", + ], + "DECREASE_LIQUIDITY": [ + "uint256", + "uint256", + "uint128", + "uint128", + "bytes", + ], + "MINT_POSITION": [ + "(address,address,uint24,int24,address)", + "int24", + "int24", + "uint256", + "uint128", + "uint128", + "address", + "bytes", + ], + "BURN_POSITION": [ + "uint256", + "uint128", + "uint128", + "bytes", + ], + "INCREASE_LIQUIDITY_FROM_DELTAS": [ + "uint256", + "uint256", + "uint128", + "uint128", + "bytes", + ], + "MINT_POSITION_FROM_DELTAS": [ + "(address,address,uint24,int24,address)", + "int24", + "int24", + "uint256", + "uint128", + "uint128", + "address", + "bytes", + ], + "SWAP_EXACT_IN_SINGLE": [ + "((address,address,uint24,int24,address),bool,uint128,uint128,uint256,bytes)", + ], + "SWAP_EXACT_IN": [ + "(address,(address,uint24,int24,address,bytes)[],int256[],uint128,uint128)", + ], + "SWAP_EXACT_OUT_SINGLE": [ + "((address,address,uint24,int24,address),bool,uint128,uint128,uint256,bytes)", + ], + "SWAP_EXACT_OUT": [ + "(address,(address,uint24,int24,address,bytes)[],int256[],uint128,uint128)", + ], + "SETTLE": [ + "address", + "uint256", + "bool", + ], + "SETTLE_ALL": [ + "address", + "uint256", + ], + "SETTLE_PAIR": [ + "address", + "address", + ], + "TAKE": [ + "address", + "address", + "uint256", + ], + "TAKE_ALL": [ + "address", + "uint256", + ], + "TAKE_PORTION": [ + "address", + "address", + "uint256", + ], + "TAKE_PAIR": [ + "address", + "address", + "address", + ], + "CLOSE_CURRENCY": [ + "address", + ], + "CLEAR_OR_TAKE": [ + "address", + "uint256", + ], + "SWEEP": [ + "address", + "address", + ], + "WRAP": [ + "uint256", + ], + "UNWRAP": [ + "uint256", + ], + "MINT_6909": [ + "address", + "uint256", + "uint256", + ], + "BURN_6909": [ + "address", + "uint256", + "uint256", + ], +} diff --git a/uniswap/decorators.py b/uniswap/decorators.py new file mode 100644 index 00000000..d886028f --- /dev/null +++ b/uniswap/decorators.py @@ -0,0 +1,72 @@ +import functools +from collections.abc import Callable +from typing import ( + TYPE_CHECKING, + Concatenate, + ParamSpec, + TypeVar, +) + +from .constants import ETH_ADDRESS +from .types import AddressLike + +if TYPE_CHECKING: + from .uniswap import Uniswap + + +T = TypeVar("T") +P = ParamSpec("P") + + +def check_approval( + method: Callable[Concatenate["Uniswap", P], T], +) -> Callable[Concatenate["Uniswap", P], T]: + """Decorator to check if user is approved for a token. It approves them if they + need to be approved.""" + + @functools.wraps(method) + def approved(self: "Uniswap", *args: P.args, **kwargs: P.kwargs) -> T: + # Check to see if the first token is actually ETH + token: AddressLike | None = args[0] if args[0] != ETH_ADDRESS else None # type: ignore + _token_two = None + + # Check second token, if needed + if method.__name__ == "make_trade" or method.__name__ == "make_trade_output": + _token_two = args[1] if args[1] != ETH_ADDRESS else None + + # Approve both tokens, if needed + if token: + is_approved = self._is_approved(token) + # logger.warning(f"Approved? {token}: {is_approved}") + if not is_approved: + self.approve(token) + return method(self, *args, **kwargs) + + return approved + + +def supports( + versions: list[int], +) -> Callable[ + [Callable[Concatenate["Uniswap", P], T]], Callable[Concatenate["Uniswap", P], T] +]: + def g( + f: Callable[Concatenate["Uniswap", P], T], + ) -> Callable[Concatenate["Uniswap", P], T]: + if f.__doc__ is None: + f.__doc__ = "" + f.__doc__ += """\n\n + Supports Uniswap + """ + ", ".join("v" + str(ver) for ver in versions) + + @functools.wraps(f) + def check_version(self: "Uniswap", *args: P.args, **kwargs: P.kwargs) -> T: + if self.version not in versions: + raise ValueError( + f"Function {f.__name__} does not support version {self.version} of Uniswap passed to constructor" + ) + return f(self, *args, **kwargs) + + return check_version + + return g diff --git a/uniswap/exceptions.py b/uniswap/exceptions.py new file mode 100644 index 00000000..efbc244c --- /dev/null +++ b/uniswap/exceptions.py @@ -0,0 +1,21 @@ +from typing import Any + + +class InvalidToken(Exception): + """Raised when an invalid token address is used.""" + + def __init__(self, address: Any) -> None: + Exception.__init__(self, f"Invalid token address: {address}") + + +class InsufficientBalance(Exception): + """Raised when the account has insufficient balance for a transaction.""" + + 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 00000000..91ae3c0b --- /dev/null +++ b/uniswap/fee.py @@ -0,0 +1,52 @@ +import enum +import logging +from typing import Final, final + +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: int | None, 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/py.typed b/uniswap/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/uniswap/token.py b/uniswap/token.py new file mode 100644 index 00000000..5f72c51f --- /dev/null +++ b/uniswap/token.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass + +from .types import AddressLike + + +@dataclass +class BaseToken: + """Base for tokens of all kinds""" + + symbol: str + """Symbol such as ETH, DAI, etc.""" + + address: AddressLike + """Address of the token contract.""" + + def __repr__(self) -> str: + return f"BaseToken({self.symbol}, {self.address!r})" + + +@dataclass +class ERC20Token(BaseToken): + """Represents an ERC20 token""" + + name: str + """Name of the token, as specified in the contract.""" + + decimals: int + """Decimals used to denominate the token.""" + + def __repr__(self) -> str: + return f"Token({self.symbol}, {self.address!r}, {self.decimals})" diff --git a/uniswap/tokens.py b/uniswap/tokens.py new file mode 100644 index 00000000..92edc63e --- /dev/null +++ b/uniswap/tokens.py @@ -0,0 +1,50 @@ +from eth_typing.evm import ChecksumAddress +from web3 import Web3 + +tokens_mainnet: dict[str, ChecksumAddress] = { + k: Web3.to_checksum_address(v) + for k, v in { + "ETH": "0x0000000000000000000000000000000000000000", + "WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "BAT": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + "WBTC": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "UNI": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", + "USDC": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + }.items() +} + +tokens_rinkeby: dict[str, ChecksumAddress] = { + k: Web3.to_checksum_address(v) + for k, v in { + "ETH": "0x0000000000000000000000000000000000000000", + "DAI": "0x2448eE2641d78CC42D7AD76498917359D961A783", + "BAT": "0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B", + }.items() +} + +tokens_arbitrum: dict[str, ChecksumAddress] = { + k: Web3.to_checksum_address(v) + for k, v in { + "ETH": "0x0000000000000000000000000000000000000000", + "WETH": "0x82af49447d8a07e3bd95bd0d56f35241523fbab1", + "DAI": "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1", + "USDC": "0xff970a61a04b1ca14834a43f5de4533ebddb5cc8", + "UNI": "0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0", + }.items() +} + + +def get_tokens(netname: str) -> dict[str, ChecksumAddress]: + """ + Returns a dict with addresses for tokens for the current net. + Used in testing. + """ + if netname == "mainnet": + return tokens_mainnet + elif netname == "rinkeby": + return tokens_rinkeby + elif netname == "arbitrum": + return tokens_arbitrum + else: + raise ValueError(f"Unknown net '{netname}'") diff --git a/uniswap/types.py b/uniswap/types.py new file mode 100644 index 00000000..659a4bf4 --- /dev/null +++ b/uniswap/types.py @@ -0,0 +1,61 @@ +from dataclasses import dataclass + +from eth_typing.evm import Address, ChecksumAddress + +AddressLike = Address | ChecksumAddress + + +# Uniswap V4 classes +@dataclass +class PoolKey: + currency0: str + currency1: str + fee: int + tick_spacing: int + hooks: str + + +@dataclass +class PathKey: + intermediate_currency: str + fee: int + tick_spacing: int + hooks: str + hook_data: bytes + + +@dataclass +class PermitDetails: + token: str + amount: int + expiration: int + nonce: int + + +@dataclass +class PermitSingle: + details: PermitDetails + spender: str + sig_deadline: int + + +@dataclass +class PermitBatch: + details: list[PermitDetails] + spender: str + sig_deadline: int + + +@dataclass +class ModifyLiquidityParams: + tick_lower: int + tick_upper: int + liquidity_delta: int + salt: int + + +@dataclass +class SwapParams: + zero_for_one: bool + amount_specified: int + sqrt_price_limit_x96: int diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index c4d3200b..5644ab8b 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1,436 +1,457 @@ +import logging import os -import json import time -import logging -import functools -from typing import List, Any, Optional, Callable, Union, Tuple, Dict +from collections import namedtuple +from collections.abc import Iterable, Sequence +from typing import ( + Any, +) +from eth_typing import HexStr +from eth_typing.evm import Address, ChecksumAddress +from hexbytes import HexBytes from web3 import Web3 -from web3.eth import Contract -from web3.contract import ContractFunction +from web3._utils.abi import map_abi_data +from web3._utils.normalizers import BASE_RETURN_NORMALIZERS +from web3.contract import Contract +from web3.contract.contract import ContractFunction +from web3.exceptions import BadFunctionCallOutput, ContractLogicError from web3.types import ( + Nonce, TxParams, + TxReceipt, Wei, - Address, - ChecksumAddress, - ENS, - Nonce, - HexBytes, ) -from eth_utils import is_same_address -from eth_typing import AnyAddress -ETH_ADDRESS = "0x0000000000000000000000000000000000000000" +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 .fee import validate_fee_tier +from .token import ERC20Token +from .types import AddressLike +from .util import ( + _addr_to_str, + _load_contract, + _load_contract_erc20, + _str_to_addr, + _validate_address, + chunks, + encode_sqrt_ratioX96, + is_same_address, + nearest_tick, + realised_fee_percentage, +) logger = logging.getLogger(__name__) -# TODO: Consider dropping support for ENS altogether and instead use AnyAddress -AddressLike = Union[Address, ChecksumAddress, ENS] - - -class InvalidToken(Exception): - def __init__(self, address: Any) -> None: - Exception.__init__(self, f"Invalid token address: {address}") - - -class InsufficientBalance(Exception): - def __init__(self, had: int, needed: int) -> None: - Exception.__init__(self, f"Insufficient balance. Had {had}, needed {needed}") - - -def _load_abi(name: str) -> str: - path = f"{os.path.dirname(os.path.abspath(__file__))}/assets/" - with open(os.path.abspath(path + f"{name}.abi")) as f: - abi: str = json.load(f) - return abi - - -def check_approval(method: Callable) -> Callable: - """Decorator to check if user is approved for a token. It approves them if they - need to be approved.""" - - @functools.wraps(method) - def approved(self: Any, *args: Any, **kwargs: Any) -> Any: - # Check to see if the first token is actually ETH - token = args[0] if args[0] != ETH_ADDRESS else None - token_two = None - - # Check second token, if needed - if method.__name__ == "make_trade" or method.__name__ == "make_trade_output": - token_two = args[1] if args[1] != ETH_ADDRESS else None - - # Approve both tokens, if needed - if token: - is_approved = self._is_approved(token) - if not is_approved: - self.approve(token) - if token_two: - is_approved = self._is_approved(token_two) - if not is_approved: - self.approve(token_two) - return method(self, *args, **kwargs) - - return approved - - -def supports(versions: List[int]) -> Callable: - def g(f: Callable) -> Callable: - @functools.wraps(f) - def check_version(self: "Uniswap", *args: List, **kwargs: Dict) -> Any: - if self.version not in versions: - raise Exception( - "Function does not support version of Uniswap passed to constructor" - ) - return f(self, *args, **kwargs) - - return check_version - - return g - - -def _str_to_addr(s: str) -> AddressLike: - if s.startswith("0x"): - return Address(bytes.fromhex(s[2:])) - elif s.endswith(".eth"): - return ENS(s) - else: - raise Exception("Could't convert string {s} to AddressLike") - - -def _addr_to_str(a: AddressLike) -> str: - if isinstance(a, bytes): - # Address or ChecksumAddress - addr: str = Web3.toChecksumAddress("0x" + bytes(a).hex()) - return addr - elif isinstance(a, str): - if a.endswith(".eth"): - # Address is ENS - raise Exception("ENS not supported for this operation") - elif a.startswith("0x"): - addr = Web3.toChecksumAddress(a) - return addr - else: - raise InvalidToken(a) - - -def _validate_address(a: AddressLike) -> None: - assert _addr_to_str(a) +class Uniswap: + """ + Wrapper around Uniswap contracts. + """ + address: AddressLike + version: int -_netid_to_name = {1: "mainnet", 4: "rinkeby"} + w3: Web3 + netid: int + netname: str + default_slippage: float + use_estimate_gas: bool -class Uniswap: def __init__( self, - address: Union[str, AddressLike], - private_key: str, - provider: str = None, - web3: Web3 = None, + address: AddressLike | str | None, + private_key: str | None, + provider: str | None = None, + web3: Web3 | None = None, version: int = 1, - max_slippage: float = 0.1, + default_slippage: float = 0.01, + use_estimate_gas: bool = True, + # use_eip1559: bool = True, + factory_contract_addr: str | None = None, + router_contract_addr: str | None = None, + enable_caching: bool = False, ) -> None: - self.address: AddressLike = _str_to_addr(address) if isinstance( - address, str - ) else address - self.private_key = private_key + """ + :param address: The public address of the ETH wallet to use. + :param private_key: The private key of the ETH wallet to use. + :param provider: Can be optionally set to a Web3 provider URI. If none set, will fall back to the PROVIDER environment variable, or web3 if set. + :param web3: Can be optionally set to a custom Web3 instance. + :param version: Which version of the Uniswap contracts to use. + :param default_slippage: Default slippage for a trade, as a float (0.01 is 1%). WARNING: slippage is untested. + :param factory_contract_addr: Can be optionally set to override the address of the factory contract. + :param router_contract_addr: Can be optionally set to override the address of the router contract (v2 only). + :param enable_caching: Optionally enables middleware caching RPC method calls. + """ + self.address = _str_to_addr( + address or "0x0000000000000000000000000000000000000000" + ) + self.private_key = ( + private_key + or "0x0000000000000000000000000000000000000000000000000000000000000000" + ) + self.version = version + if self.version not in [1, 2, 3]: + raise ValueError( + f"Invalid version '{self.version}', only 1, 2 or 3 supported" + ) # pragma: no cover - # TODO: Write tests for slippage - self.max_slippage = max_slippage + self.default_slippage = default_slippage + self.use_estimate_gas = use_estimate_gas if web3: self.w3 = web3 else: # Initialize web3. Extra provider for testing. - self.provider = provider or os.environ["PROVIDER"] - self.w3 = Web3( - Web3.HTTPProvider(self.provider, request_kwargs={"timeout": 60}) - ) + if not provider: + provider = os.environ["PROVIDER"] + self.w3 = Web3(Web3.HTTPProvider(provider, request_kwargs={"timeout": 60})) - netid = int(self.w3.net.version) - if netid in _netid_to_name: - self.network = _netid_to_name[netid] + self.netid = int(self.w3.net.version) + if self.netid in _netid_to_name: + self.netname = _netid_to_name[self.netid] else: - raise Exception(f"Unknown netid: {netid}") - logger.info(f"Using {self.w3} ('{self.network}')") + raise ValueError(f"Unknown netid: {self.netid}") # pragma: no cover + logger.info(f"Using {self.w3} ('{self.netname}', netid: {self.netid})") - self.last_nonce: Nonce = self.w3.eth.getTransactionCount(self.address) + self.last_nonce: Nonce = self.w3.eth.get_transaction_count(self.address) # This code automatically approves you for trading on the exchange. # max_approval is to allow the contract to exchange on your behalf. # max_approval_check checks that current approval is above a reasonable number # The program cannot check for max_approval each time because it decreases # with each trade. - self.max_approval_hex = f"0x{64 * 'f'}" - self.max_approval_int = int(self.max_approval_hex, 16) - self.max_approval_check_hex = f"0x{15 * '0'}{49 * 'f'}" - self.max_approval_check_int = int(self.max_approval_check_hex, 16) + max_approval_hex = f"0x{64 * 'f'}" + self.max_approval_int = int(max_approval_hex, 16) + max_approval_check_hex = f"0x{15 * '0'}{49 * 'f'}" + self.max_approval_check_int = int(max_approval_check_hex, 16) if self.version == 1: - factory_contract_addresses = { - "mainnet": "0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95", - "ropsten": "0x9c83dCE8CA20E9aAF9D3efc003b2ea62aBC08351", - "rinkeby": "0xf5D915570BC477f9B8D6C0E980aA81757A3AaC36", - "kovan": "0xD3E51Ef092B2845f10401a0159B2B96e8B6c3D30", - "görli": "0x6Ce570d02D73d4c384b46135E87f8C592A8c86dA", - } - - self.factory_contract = self._load_contract( + if factory_contract_addr is None: + factory_contract_addr = _factory_contract_addresses_v1[self.netname] + + self.factory_contract = _load_contract( + self.w3, abi_name="uniswap-v1/factory", - address=_str_to_addr(factory_contract_addresses[self.network]), + address=_str_to_addr(factory_contract_addr), ) elif self.version == 2: - # For v2 the address is the same on mainnet, Ropsten, Rinkeby, Görli, and Kovan - # https://uniswap.org/docs/v2/smart-contracts/factory - factory_contract_address_v2 = _str_to_addr( - "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f" + if router_contract_addr is None: + router_contract_addr = _router_contract_addresses_v2[self.netname] + self.router_address: AddressLike = _str_to_addr(router_contract_addr) + + if factory_contract_addr is None: + factory_contract_addr = _factory_contract_addresses_v2[self.netname] + self.factory_contract = _load_contract( + self.w3, + abi_name="uniswap-v2/factory", + address=_str_to_addr(factory_contract_addr), + ) + # Documented here: https://uniswap.org/docs/v2/smart-contracts/router02/ + self.router = _load_contract( + self.w3, + abi_name="uniswap-v2/router02", + address=self.router_address, + ) + elif self.version == 3: + # https://github.com/Uniswap/uniswap-v3-periphery/blob/main/deploys.md + factory_contract_address = _str_to_addr( + "0x1F98431c8aD98523631AE4a59f267346ea31F984" + ) + self.factory_contract = _load_contract( + self.w3, abi_name="uniswap-v3/factory", address=factory_contract_address + ) + quoter_addr = _str_to_addr("0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6") + self.router_address = _str_to_addr( + "0xE592427A0AEce92De3Edee1F18E0157C05861564" + ) + self.quoter = _load_contract( + self.w3, abi_name="uniswap-v3/quoter", address=quoter_addr + ) + self.router = _load_contract( + self.w3, abi_name="uniswap-v3/router", address=self.router_address ) - self.factory_contract = self._load_contract( - abi_name="uniswap-v2/factory", address=factory_contract_address_v2, + self.positionManager_addr = _str_to_addr( + "0xC36442b4a4522E871399CD717aBDD847Ab11FE88" ) - self.router_address: AddressLike = _str_to_addr( - "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" + self.nonFungiblePositionManager = _load_contract( + self.w3, + abi_name="uniswap-v3/nonFungiblePositionManager", + address=self.positionManager_addr, ) - """Documented here: https://uniswap.org/docs/v2/smart-contracts/router02/""" - self.router = self._load_contract( - abi_name="uniswap-v2/router02", address=self.router_address, + if self.netname == "arbitrum": + multicall2_addr = _str_to_addr( + "0x50075F151ABC5B6B448b1272A0a1cFb5CFA25828" + ) + else: + multicall2_addr = _str_to_addr( + "0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696" + ) + self.multicall2 = _load_contract( + self.w3, abi_name="uniswap-v3/multicall", address=multicall2_addr ) else: - raise Exception("Invalid version, only 1 or 2 supported") - - logger.info(f"Using factory contract: {self.factory_contract}") - - @supports([1]) - def get_all_tokens(self) -> List[dict]: - # FIXME: This is a very expensive operation, would benefit greatly from caching - tokenCount = self.factory_contract.functions.tokenCount().call() - tokens = [] - for i in range(tokenCount): - address = self.factory_contract.functions.getTokenWithId(i).call() - if address == "0x0000000000000000000000000000000000000000": - # Token is ETH - continue - token = self.get_token(address) - tokens.append(token) - return tokens - - @supports([1]) - def get_token(self, address: AddressLike) -> dict: - # FIXME: This function should always return the same output for the same input - # and would therefore benefit from caching - token_contract = self._load_contract(abi_name="erc20", address=address) - try: - symbol = token_contract.functions.symbol().call() - name = token_contract.functions.name().call() - except Exception as e: - logger.warning( - f"Exception occurred while trying to get token {_addr_to_str(address)}: {e}" + raise ValueError( + f"Invalid version '{self.version}', only 1, 2 or 3 supported" ) - raise InvalidToken(address) - return {"name": name, "symbol": symbol} - @supports([1]) - def exchange_address_from_token(self, token_addr: AddressLike) -> AddressLike: - ex_addr: AddressLike = self.factory_contract.functions.getExchange( - token_addr - ).call() - # TODO: What happens if the token doesn't have an exchange/doesn't exist? Should probably raise an Exception (and test it) - return ex_addr - - @supports([1]) - def token_address_from_exchange(self, exchange_addr: AddressLike) -> Address: - token_addr: Address = ( - self.exchange_contract(ex_addr=exchange_addr) - .functions.tokenAddress(exchange_addr) - .call() - ) - return token_addr - - @functools.lru_cache() - @supports([1]) - def exchange_contract( - self, token_addr: AddressLike = None, ex_addr: AddressLike = None - ) -> Contract: - if not ex_addr and token_addr: - ex_addr = self.exchange_address_from_token(token_addr) - if ex_addr is None: - raise InvalidToken(token_addr) - abi_name = "uniswap-v1/exchange" - contract = self._load_contract(abi_name=abi_name, address=ex_addr) - logger.info(f"Loaded exchange contract {contract} at {contract.address}") - return contract + if hasattr(self, "factory_contract"): + logger.info(f"Using factory contract: {self.factory_contract}") - @functools.lru_cache() - def erc20_contract(self, token_addr: AddressLike) -> Contract: - return self._load_contract(abi_name="erc20", address=token_addr) + # ------ Market -------------------------------------------------------------------- - @functools.lru_cache() - @supports([2]) - def get_weth_address(self) -> ChecksumAddress: - # Contract calls should always return checksummed addresses - address: ChecksumAddress = self.router.functions.WETH().call() - return address + def get_price_input( + self, + token0: AddressLike, # input token + token1: AddressLike, # output token + qty: int, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> int: + """Given `qty` amount of the input `token0`, returns the maximum output amount of output `token1`.""" + fee = validate_fee_tier(fee=fee, version=self.version) - def _load_contract(self, abi_name: str, address: AddressLike) -> Contract: - return self.w3.eth.contract(address=address, abi=_load_abi(abi_name)) + if is_same_address(token0, ETH_ADDRESS) and self.version == 2: + return self._get_eth_token_input_price(token1, Wei(qty), fee) + elif is_same_address(token1, ETH_ADDRESS) and self.version == 2: + return self._get_token_eth_input_price(token0, qty, fee) + else: + if is_same_address(token0, ETH_ADDRESS): + token0 = self.get_weth_address() + if is_same_address(token1, ETH_ADDRESS): + token1 = self.get_weth_address() + return self._get_token_token_input_price( + token0, token1, qty, fee, route, fees + ) - # ------ Exchange ------------------------------------------------------------------ - @supports([1, 2]) - def get_fee_maker(self) -> float: - """Get the maker fee.""" - return 0 + def get_price_output( + self, + token0: AddressLike, + token1: AddressLike, + qty: int, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, + ) -> int: + """Returns the minimum amount of `token0` required to buy `qty` amount of `token1`.""" + fee = validate_fee_tier(fee=fee, version=self.version) - @supports([1, 2]) - def get_fee_taker(self) -> float: - """Get the taker fee.""" - return 0.003 + if is_same_address(token0, ETH_ADDRESS) and self.version == 2: + return self._get_eth_token_output_price(token1, qty, fee) + elif is_same_address(token1, ETH_ADDRESS) and self.version == 2: + return self._get_token_eth_output_price(token0, Wei(qty), fee) + else: + if is_same_address(token0, ETH_ADDRESS): + token0 = self.get_weth_address() + if is_same_address(token1, ETH_ADDRESS): + token1 = self.get_weth_address() + return self._get_token_token_output_price( + token0, token1, qty, fee, route, fees + ) - # ------ Market -------------------------------------------------------------------- - @supports([1, 2]) - def get_eth_token_input_price(self, token: AddressLike, qty: Wei) -> Wei: - """Public price for ETH to Token trades with an exact input.""" + def _get_eth_token_input_price( + self, + token: AddressLike, # output token + qty: Wei, + fee: int, + ) -> Wei: + """Public price (i.e. amount of output token received) for ETH to token trades with an exact input.""" if self.version == 1: - ex = self.exchange_contract(token) + ex = self._exchange_contract(token) price: Wei = ex.functions.getEthToTokenInputPrice(qty).call() elif self.version == 2: price = self.router.functions.getAmountsOut( qty, [self.get_weth_address(), token] ).call()[-1] + elif self.version == 3: + price = self._get_token_token_input_price( + self.get_weth_address(), token, qty, fee=fee + ) # type: ignore + else: + raise ValueError # pragma: no cover return price - @supports([1, 2]) - def get_token_eth_input_price(self, token: AddressLike, qty: int) -> int: - """Public price for token to ETH trades with an exact input.""" + def _get_token_eth_input_price( + self, + token: AddressLike, # input token + qty: int, + fee: int, + ) -> int: + """Public price (i.e. amount of ETH received) for token to ETH trades with an exact input.""" if self.version == 1: - ex = self.exchange_contract(token) + ex = self._exchange_contract(token) price: int = ex.functions.getTokenToEthInputPrice(qty).call() - else: + elif self.version == 2: price = self.router.functions.getAmountsOut( qty, [token, self.get_weth_address()] ).call()[-1] + elif self.version == 3: + price = self._get_token_token_input_price( + token, self.get_weth_address(), qty, fee=fee + ) + else: + raise ValueError # pragma: no cover return price - @supports([2]) - def get_token_token_input_price( - self, token0: AnyAddress, token1: AnyAddress, qty: int + def _get_token_token_input_price( + self, + token0: AddressLike, # input token + token1: AddressLike, # output token + qty: int, + fee: int, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> int: - """Public price for token to token trades with an exact input.""" - # If one of the tokens are WETH, delegate to appropriate call. - # See: https://github.com/shanefontaine/uniswap-python/issues/22 - if is_same_address(token0, self.get_weth_address()): - return int(self.get_eth_token_input_price(token1, qty)) - elif is_same_address(token1, self.get_weth_address()): - return int(self.get_token_eth_input_price(token0, qty)) - - price: int = self.router.functions.getAmountsOut( - qty, [token0, self.get_weth_address(), token1] - ).call()[-1] + """ + Public price (i.e. amount of output token received) for token to token trades with an exact input. + + :param fee: (v3 only) The pool's fee in hundredths of a bip, i.e. 1e-6 (3000 is 0.3%) + """ + if route is None and self.version == 2: + # If one of the tokens are WETH, delegate to appropriate call. + # See: https://github.com/shanefontaine/uniswap-python/issues/22 + if is_same_address(token0, self.get_weth_address()): + return int(self._get_eth_token_input_price(token1, Wei(qty), fee)) + elif is_same_address(token1, self.get_weth_address()): + return int(self._get_token_eth_input_price(token0, qty, fee)) + + route = [token0, self.get_weth_address(), token1] + logger.warning(f"No route specified, assuming route: {route}") + + if self.version == 2: + price: int = self.router.functions.getAmountsOut(qty, route).call()[-1] + elif self.version == 3: + if route and not fees: + raise ValueError( + "V3 custom routes require both 'route' and 'fees' parameters" + ) + if route and fees: + # 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.quoteExactInput(route, qty).call() + encoded_v3_path = self.encode_v3_path(route, fees) + price = self.quoter.functions.quoteExactInput( + encoded_v3_path, qty + ).call() + else: + # FIXME: How to calculate this properly? See https://docs.uniswap.org/reference/libraries/SqrtPriceMath + sqrtPriceLimitX96 = 0 + price = self.quoter.functions.quoteExactInputSingle( + token0, token1, fee, qty, sqrtPriceLimitX96 + ).call() + else: + raise ValueError("function not supported for this version of Uniswap") return price - @supports([1, 2]) - def get_eth_token_output_price(self, token: AddressLike, qty: int) -> Wei: - """Public price for ETH to Token trades with an exact output.""" + def _get_eth_token_output_price( + self, + token: AddressLike, # output token + qty: int, + fee: int | None = 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) + ex = self._exchange_contract(token) price: Wei = ex.functions.getEthToTokenOutputPrice(qty).call() + elif self.version == 2: + route = [self.get_weth_address(), token] + price = self.router.functions.getAmountsIn(qty, route).call()[0] + elif self.version == 3: + price = Wei( + self._get_token_token_output_price( + self.get_weth_address(), token, qty, fee=fee + ) + ) else: - price = self.router.functions.getAmountsIn( - qty, [self.get_weth_address(), token] - ).call()[0] + raise ValueError # pragma: no cover return price - @supports([1, 2]) - def get_token_eth_output_price(self, token: AddressLike, qty: Wei) -> int: - """Public price for token to ETH trades with an exact output.""" + def _get_token_eth_output_price( + self, + token: AddressLike, + qty: Wei, + fee: int | None = 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) + ex = self._exchange_contract(token) price: int = ex.functions.getTokenToEthOutputPrice(qty).call() + elif self.version == 2: + route = [token, self.get_weth_address()] + price = self.router.functions.getAmountsIn(qty, route).call()[0] + elif self.version == 3: + price = self._get_token_token_output_price( + token, self.get_weth_address(), qty, fee=fee + ) else: - price = self.router.functions.getAmountsIn( - qty, [token, self.get_weth_address()] - ).call()[0] + raise ValueError # pragma: no cover return price - @supports([2]) - def get_token_token_output_price( - self, token0: AnyAddress, token1: AnyAddress, qty: int + @supports([2, 3]) + def _get_token_token_output_price( + self, + token0: AddressLike, # input token + token1: AddressLike, # output token + qty: int, + fee: int | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> int: - """Public price for token to token trades with an exact output.""" - # If one of the tokens are WETH, delegate to appropriate call. - # See: https://github.com/shanefontaine/uniswap-python/issues/22 - # TODO: Will these equality checks always work? (Address vs ChecksumAddress vs str) - if is_same_address(token0, self.get_weth_address()): - return int(self.get_eth_token_output_price(token1, qty)) - elif is_same_address(token1, self.get_weth_address()): - return int(self.get_token_eth_output_price(token0, qty)) - - price: int = self.router.functions.getAmountsIn( - qty, [token0, self.get_weth_address(), token1] - ).call()[0] - return price - - # ------ Wallet balance ------------------------------------------------------------ - def get_eth_balance(self) -> Wei: - """Get the balance of ETH in a wallet.""" - return self.w3.eth.getBalance(self.address) - - def get_token_balance(self, token: AddressLike) -> int: - """Get the balance of a token in a wallet.""" - _validate_address(token) - if _addr_to_str(token) == ETH_ADDRESS: - return self.get_eth_balance() - erc20 = self.erc20_contract(token) - balance: int = erc20.functions.balanceOf(self.address).call() - return balance - - # ------ ERC20 Pool ---------------------------------------------------------------- - @supports([1]) - def get_ex_eth_balance(self, token: AddressLike) -> int: - """Get the balance of ETH in an exchange contract.""" - ex_addr: AddressLike = self.exchange_address_from_token(token) - return self.w3.eth.getBalance(ex_addr) - - @supports([1]) - def get_ex_token_balance(self, token: AddressLike) -> int: - """Get the balance of a token in an exchange contract.""" - erc20 = self.erc20_contract(token) - balance: int = erc20.functions.balanceOf( - self.exchange_address_from_token(token) - ).call() - return balance - - # TODO: ADD TOTAL SUPPLY - @supports([1]) - def get_exchange_rate(self, token: AddressLike) -> float: - """Get the current ETH/token exchange rate of the token.""" - eth_reserve = self.get_ex_eth_balance(token) - token_reserve = self.get_ex_token_balance(token) - return float(token_reserve / eth_reserve) - - # ------ Liquidity ----------------------------------------------------------------- - @supports([1]) - @check_approval - def add_liquidity( - self, token: AddressLike, max_eth: Wei, min_liquidity: int = 1 - ) -> HexBytes: - """Add liquidity to the pool.""" - tx_params = self._get_tx_params(max_eth) - # Add 1 to avoid rounding errors, per - # https://hackmd.io/hthz9hXKQmSyXfMbPsut1g#Add-Liquidity-Calculations - max_token = int(max_eth * self.get_exchange_rate(token)) + 10 - func_params = [min_liquidity, max_token, self._deadline()] - function = self.exchange_contract(token).functions.addLiquidity(*func_params) - return self._build_and_send_tx(function, tx_params) + """ + Public price (i.e. amount of input token needed) for token to token trades with an exact output. - @supports([1]) - @check_approval - def remove_liquidity(self, token: str, max_token: int) -> HexBytes: - """Remove liquidity from the pool.""" - func_params = [int(max_token), 1, 1, self._deadline()] - function = self.exchange_contract(token).functions.removeLiquidity(*func_params) - return self._build_and_send_tx(function) + :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 and self.version == 2: + # If one of the tokens are WETH, delegate to appropriate call. + # See: https://github.com/shanefontaine/uniswap-python/issues/22 + if is_same_address(token0, self.get_weth_address()): + return int(self._get_eth_token_output_price(token1, qty, fee)) + elif is_same_address(token1, self.get_weth_address()): + return int(self._get_token_eth_output_price(token0, Wei(qty), fee)) + + route = [token0, self.get_weth_address(), token1] + logger.warning(f"No route specified, assuming route: {route}") + + if self.version == 2: + price: int = self.router.functions.getAmountsIn(qty, route).call()[0] + elif self.version == 3: + if route and fees: + # 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() + encoded_v3_path = self.encode_v3_path(route, fees, is_exact_out=True) + price = self.quoter.functions.quoteExactOutput( + encoded_v3_path, qty + ).call() + else: + # FIXME: How to calculate this properly? + # - https://docs.uniswap.org/reference/libraries/SqrtPriceMath + # - https://github.com/Uniswap/uniswap-v3-sdk/blob/main/src/swapRouter.ts + sqrtPriceLimitX96 = 0 + price = self.quoter.functions.quoteExactOutputSingle( + token0, token1, fee, qty, sqrtPriceLimitX96 + ).call() + else: + raise ValueError # pragma: no cover + return price # ------ Make Trade ---------------------------------------------------------------- @check_approval @@ -438,74 +459,144 @@ def make_trade( self, input_token: AddressLike, output_token: AddressLike, - qty: Union[int, Wei], - recipient: AddressLike = None, + qty: int | Wei, + recipient: AddressLike | None = None, + fee: int | None = None, + slippage: float | None = None, + fee_on_transfer: bool = False, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> HexBytes: """Make a trade by defining the qty of the input token.""" - if input_token == ETH_ADDRESS: - return self._eth_to_token_swap_input(output_token, Wei(qty), recipient) + if not isinstance(qty, int): + raise TypeError("swapped quantity must be an integer") + + fee = validate_fee_tier(fee=fee, version=self.version) + + if slippage is None: + slippage = self.default_slippage + + if input_token == output_token: + raise ValueError + + if self.version != 3 and fees: + fees = None + + if self.version == 3 and route and not fees: + raise ValueError( + "V3 custom routes require both 'route' and 'fees' parameters" + ) + + if input_token == ETH_ADDRESS and (route is None or fees is None): + return self._eth_to_token_swap_input( + output_token, Wei(qty), recipient, fee, slippage, fee_on_transfer + ) + elif output_token == ETH_ADDRESS and (route is None or fees is None): + return self._token_to_eth_swap_input( + input_token, qty, recipient, fee, slippage, fee_on_transfer + ) else: - balance = self.get_token_balance(input_token) - if balance < qty: - raise InsufficientBalance(balance, qty) - if output_token == ETH_ADDRESS: - return self._token_to_eth_swap_input(input_token, qty, recipient) - else: - return self._token_to_token_swap_input( - input_token, qty, output_token, recipient - ) + # if is_same_address(input_token, ETH_ADDRESS): + # input_token = self.get_weth_address() + # if is_same_address(output_token, ETH_ADDRESS): + # output_token = self.get_weth_address() + return self._token_to_token_swap_input( + input_token, + output_token, + qty, + recipient, + fee, + slippage, + fee_on_transfer, + route, + fees, + ) @check_approval def make_trade_output( self, input_token: AddressLike, output_token: AddressLike, - qty: Union[int, Wei], - recipient: AddressLike = None, + qty: int | Wei, + recipient: AddressLike | None = None, + fee: int | None = None, + slippage: float | None = None, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> HexBytes: """Make a trade by defining the qty of the output token.""" - if input_token == ETH_ADDRESS: + fee = validate_fee_tier(fee=fee, version=self.version) + + if slippage is None: + slippage = self.default_slippage + + if input_token == output_token: + raise ValueError + + if self.version != 3 and fees: + fees = None + + if input_token == ETH_ADDRESS and (route is None or fees is None): balance = self.get_eth_balance() - need = self.get_eth_token_output_price(output_token, qty) + need = self._get_eth_token_output_price(output_token, qty, fee) if balance < need: raise InsufficientBalance(balance, need) - return self._eth_to_token_swap_output(output_token, qty, recipient) + return self._eth_to_token_swap_output( + output_token, qty, recipient, fee, slippage + ) + elif output_token == ETH_ADDRESS and (route is None or fees is None): + return self._token_to_eth_swap_output( + input_token, Wei(qty), recipient, fee, slippage + ) else: - if output_token == ETH_ADDRESS: - qty = Wei(qty) - return self._token_to_eth_swap_output(input_token, qty, recipient) - else: - return self._token_to_token_swap_output( - input_token, qty, output_token, recipient - ) + # if is_same_address(input_token, ETH_ADDRESS): + # input_token = self.get_weth_address() + # if is_same_address(output_token, ETH_ADDRESS): + # output_token = self.get_weth_address() + return self._token_to_token_swap_output( + input_token, output_token, qty, recipient, fee, slippage, route, fees + ) def _eth_to_token_swap_input( - self, output_token: AddressLike, qty: Wei, recipient: Optional[AddressLike] + self, + output_token: AddressLike, + qty: Wei, + recipient: AddressLike | None, + fee: int, + slippage: float, + fee_on_transfer: bool = False, ) -> HexBytes: """Convert ETH to tokens given an input amount.""" + if output_token == ETH_ADDRESS: + raise ValueError + eth_balance = self.get_eth_balance() if qty > eth_balance: raise InsufficientBalance(eth_balance, qty) if self.version == 1: - token_funcs = self.exchange_contract(output_token).functions + token_funcs = self._exchange_contract(output_token).functions tx_params = self._get_tx_params(qty) - func_params: List[Any] = [qty, self._deadline()] + func_params: list[Any] = [qty, self._deadline()] if not recipient: function = token_funcs.ethToTokenSwapInput(*func_params) else: func_params.append(recipient) function = token_funcs.ethToTokenTransferInput(*func_params) return self._build_and_send_tx(function, tx_params) - else: + + elif self.version == 2: if recipient is None: recipient = self.address amount_out_min = int( - (1 - self.max_slippage) - * self.get_eth_token_input_price(output_token, qty) + (1 - slippage) * self._get_eth_token_input_price(output_token, qty, fee) ) + if fee_on_transfer: + func = self.router.functions.swapExactETHForTokensSupportingFeeOnTransferTokens + else: + func = self.router.functions.swapExactETHForTokens return self._build_and_send_tx( - self.router.functions.swapExactETHForTokens( + func( amount_out_min, [self.get_weth_address(), output_token], recipient, @@ -513,34 +604,76 @@ def _eth_to_token_swap_input( ), self._get_tx_params(qty), ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + if fee_on_transfer: + raise ValueError("fee on transfer not supported by Uniswap v3") + + min_tokens_bought = int( + (1 - slippage) + * self._get_eth_token_input_price(output_token, qty, fee=fee) + ) + sqrtPriceLimitX96 = 0 + + return self._build_and_send_tx( + self.router.functions.exactInputSingle( + { + "tokenIn": self.get_weth_address(), + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + } + ), + self._get_tx_params(value=qty), + ) + else: + raise ValueError # pragma: no cover def _token_to_eth_swap_input( - self, input_token: AddressLike, qty: int, recipient: Optional[AddressLike] + self, + input_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + fee_on_transfer: bool = False, ) -> HexBytes: """Convert tokens to ETH given an input amount.""" + if input_token == ETH_ADDRESS: + raise ValueError + # Balance check input_balance = self.get_token_balance(input_token) if qty > input_balance: raise InsufficientBalance(input_balance, qty) if self.version == 1: - token_funcs = self.exchange_contract(input_token).functions - func_params: List[Any] = [qty, 1, self._deadline()] + token_funcs = self._exchange_contract(input_token).functions + func_params: list[Any] = [qty, 1, self._deadline()] if not recipient: function = token_funcs.tokenToEthSwapInput(*func_params) else: func_params.append(recipient) function = token_funcs.tokenToEthTransferInput(*func_params) return self._build_and_send_tx(function) - else: + elif self.version == 2: if recipient is None: recipient = self.address amount_out_min = int( - (1 - self.max_slippage) - * self.get_token_eth_input_price(input_token, qty) + (1 - slippage) * self._get_token_eth_input_price(input_token, qty, fee) ) + if fee_on_transfer: + func = self.router.functions.swapExactTokensForETHSupportingFeeOnTransferTokens + else: + func = self.router.functions.swapExactTokensForETH return self._build_and_send_tx( - self.router.functions.swapExactTokensForETH( + func( qty, amount_out_min, [input_token, self.get_weth_address()], @@ -548,19 +681,78 @@ def _token_to_eth_swap_input( self._deadline(), ), ) + elif self.version == 3: + if recipient is None: + recipient = self.address - def _token_to_token_swap_input( - self, - input_token: AddressLike, - qty: int, - output_token: AddressLike, - recipient: Optional[AddressLike], + if fee_on_transfer: + raise ValueError("fee on transfer not supported by Uniswap v3") + + output_token = self.get_weth_address() + min_tokens_bought = int( + (1 - slippage) + * self._get_token_eth_input_price(input_token, qty, fee=fee) + ) + sqrtPriceLimitX96 = 0 + + swap_data = self.router.encode_abi( + "exactInputSingle", + args=[ + ( + input_token, + output_token, + fee, + ETH_ADDRESS, + self._deadline(), + qty, + min_tokens_bought, + sqrtPriceLimitX96, + ) + ], + ) + + unwrap_data = self.router.encode_abi( + "unwrapWETH9", args=[min_tokens_bought, recipient] + ) + + # Multicall + return self._build_and_send_tx( + self.router.functions.multicall([swap_data, unwrap_data]), + self._get_tx_params(), + ) + else: + raise ValueError # pragma: no cover + + def _token_to_token_swap_input( + self, + input_token: AddressLike, + output_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + fee_on_transfer: bool = False, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> HexBytes: """Convert tokens to tokens given an input amount.""" + # Balance check + input_balance = self.get_token_balance(input_token) + if qty > input_balance: + raise InsufficientBalance(input_balance, qty) + + if recipient is None: + recipient = self.address + + if (input_token == ETH_ADDRESS or output_token == ETH_ADDRESS) and ( + route is None or fees is None + ): + raise ValueError + if self.version == 1: - token_funcs = self.exchange_contract(input_token).functions + token_funcs = self._exchange_contract(input_token).functions # TODO: This might not be correct - min_tokens_bought, min_eth_bought = self._calculate_max_input_token( + min_tokens_bought, min_eth_bought = self._calculate_max_output_token( input_token, qty, output_token ) func_params = [ @@ -576,42 +768,119 @@ def _token_to_token_swap_input( func_params.insert(len(func_params) - 1, recipient) function = token_funcs.tokenToTokenTransferInput(*func_params) return self._build_and_send_tx(function) - else: - if recipient is None: - recipient = self.address + elif self.version == 2: + if fee_on_transfer: + func = self.router.functions.swapExactTokensForTokensSupportingFeeOnTransferTokens + else: + func = self.router.functions.swapExactTokensForTokens + if route is None: + weth_address = self.get_weth_address() + if is_same_address(input_token, weth_address) or is_same_address( + output_token, weth_address + ): + path = [input_token, output_token] + else: + path = [input_token, weth_address, output_token] + else: + path = route min_tokens_bought = int( - (1 - self.max_slippage) - * self.get_token_token_input_price(input_token, output_token, qty) + (1 - slippage) + * self._get_token_token_input_price( + input_token, output_token, qty, fee=fee, route=path, fees=fees + ) ) return self._build_and_send_tx( - self.router.functions.swapExactTokensForTokens( + func( qty, min_tokens_bought, - [input_token, self.get_weth_address(), output_token], + path, recipient, self._deadline(), ), ) + elif self.version == 3: + if fee_on_transfer: + raise ValueError("fee on transfer not supported by Uniswap v3") + + min_tokens_bought = int( + (1 - slippage) + * self._get_token_token_input_price( + input_token, output_token, qty, fee=fee, route=route, fees=fees + ) + ) + sqrtPriceLimitX96 = 0 + if route and fees: + encoded_v3_path = self.encode_v3_path(route, fees) + return self._build_and_send_tx( + self.router.functions.exactInput( + { + "path": encoded_v3_path, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + } + ), + self._get_tx_params(), + ) + else: + return self._build_and_send_tx( + self.router.functions.exactInputSingle( + { + "tokenIn": input_token, + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + } + ), + self._get_tx_params(), + ) + else: + raise ValueError # pragma: no cover def _eth_to_token_swap_output( - self, output_token: AddressLike, qty: int, recipient: Optional[AddressLike] + self, + output_token: AddressLike, + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, ) -> HexBytes: """Convert ETH to tokens given an output amount.""" + if output_token == ETH_ADDRESS: + raise ValueError + + # Balance check + eth_balance = self.get_eth_balance() + cost = self._get_eth_token_output_price(output_token, qty, fee) + amount_in_max = Wei(int((1 + slippage) * cost)) + + # We check balance against amount_in_max rather than cost to be conservative + if amount_in_max > eth_balance: + raise InsufficientBalance(eth_balance, amount_in_max) + if self.version == 1: - token_funcs = self.exchange_contract(output_token).functions - eth_qty = self.get_eth_token_output_price(output_token, qty) + token_funcs = self._exchange_contract(output_token).functions + eth_qty = self._get_eth_token_output_price(output_token, qty) tx_params = self._get_tx_params(eth_qty) - func_params: List[Any] = [qty, self._deadline()] + func_params: list[Any] = [qty, self._deadline()] if not recipient: function = token_funcs.ethToTokenSwapOutput(*func_params) else: func_params.append(recipient) function = token_funcs.ethToTokenTransferOutput(*func_params) return self._build_and_send_tx(function, tx_params) - else: + elif self.version == 2: if recipient is None: recipient = self.address - eth_qty = self.get_eth_token_output_price(output_token, qty) + eth_qty = int( + (1 + slippage) + * self._get_eth_token_output_price(output_token, qty, fee) + ) # type: ignore return self._build_and_send_tx( self.router.functions.swapETHForExactTokens( qty, @@ -621,20 +890,60 @@ def _eth_to_token_swap_output( ), self._get_tx_params(eth_qty), ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + sqrtPriceLimitX96 = 0 + + swap_data = self.router.encode_abi( + "exactOutputSingle", + args=[ + ( + self.get_weth_address(), + output_token, + fee, + recipient, + self._deadline(), + qty, + amount_in_max, + sqrtPriceLimitX96, + ) + ], + ) + + refund_data = self.router.encode_abi("refundETH", args=None) + + # Multicall + return self._build_and_send_tx( + self.router.functions.multicall([swap_data, refund_data]), + self._get_tx_params(value=amount_in_max), + ) + else: + raise ValueError def _token_to_eth_swap_output( - self, input_token: AddressLike, qty: Wei, recipient: Optional[AddressLike] + self, + input_token: AddressLike, + qty: Wei, + recipient: AddressLike | None, + fee: int, + slippage: float, ) -> HexBytes: """Convert tokens to ETH given an output amount.""" + if input_token == ETH_ADDRESS: + raise ValueError + # Balance check input_balance = self.get_token_balance(input_token) - cost = self.get_token_eth_output_price(input_token, qty) - if cost > input_balance: - raise InsufficientBalance(input_balance, cost) + cost = self._get_token_eth_output_price(input_token, qty, fee) + amount_in_max = int((1 + slippage) * cost) - if self.version == 1: - token_funcs = self.exchange_contract(input_token).functions + # We check balance against amount_in_max rather than cost to be conservative + if amount_in_max > input_balance: + raise InsufficientBalance(input_balance, amount_in_max) + if self.version == 1: # From https://uniswap.org/docs/v1/frontend-integration/trade-tokens/ # Is all this really necessary? Can't we just use `cost` for max_tokens? outputAmount = qty @@ -645,37 +954,95 @@ def _token_to_eth_swap_output( denominator = (outputReserve - outputAmount) * 997 inputAmount = numerator / denominator + 1 - max_tokens = int((1 + self.max_slippage) * inputAmount) + max_tokens = int((1 + slippage) * inputAmount) - func_params: List[Any] = [qty, max_tokens, self._deadline()] + ex = self._exchange_contract(input_token) + func_params: list[Any] = [qty, max_tokens, self._deadline()] if not recipient: - function = token_funcs.tokenToEthSwapOutput(*func_params) + function = ex.functions.tokenToEthSwapOutput(*func_params) else: func_params.append(recipient) - function = token_funcs.tokenToEthTransferOutput(*func_params) + function = ex.functions.tokenToEthTransferOutput(*func_params) return self._build_and_send_tx(function) - else: - max_tokens = int((1 + self.max_slippage) * cost) + elif self.version == 2: + if recipient is None: + recipient = self.address + + max_tokens = int((1 + slippage) * cost) return self._build_and_send_tx( self.router.functions.swapTokensForExactETH( qty, max_tokens, [input_token, self.get_weth_address()], - self.address, + recipient, self._deadline(), ), ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + sqrtPriceLimitX96 = 0 + + swap_data = self.router.encode_abi( + "exactOutputSingle", + args=[ + ( + input_token, + self.get_weth_address(), + fee, + ETH_ADDRESS, + self._deadline(), + qty, + amount_in_max, + sqrtPriceLimitX96, + ) + ], + ) + + unwrap_data = self.router.encode_abi("unwrapWETH9", args=[qty, recipient]) + + # Multicall + return self._build_and_send_tx( + self.router.functions.multicall([swap_data, unwrap_data]), + self._get_tx_params(), + ) + else: + raise ValueError def _token_to_token_swap_output( self, input_token: AddressLike, - qty: int, output_token: AddressLike, - recipient: Optional[AddressLike], + qty: int, + recipient: AddressLike | None, + fee: int, + slippage: float, + route: list[AddressLike] | None = None, + fees: list[int] | None = None, ) -> HexBytes: - """Convert tokens to tokens given an output amount.""" + """Convert tokens to tokens given an output amount. + + :param fee: TODO + """ + if (input_token == ETH_ADDRESS or output_token == ETH_ADDRESS) and ( + route is None or fees is None + ): + raise ValueError + + # Balance check + input_balance = self.get_token_balance(input_token) + cost = self._get_token_token_output_price( + input_token, output_token, qty, fee, route, fees + ) + amount_in_max = int((1 + slippage) * cost) + if ( + amount_in_max > input_balance + ): # We check balance against amount_in_max rather than cost to be conservative + raise InsufficientBalance(input_balance, amount_in_max) + if self.version == 1: - token_funcs = self.exchange_contract(input_token).functions + token_funcs = self._exchange_contract(input_token).functions max_tokens_sold, max_eth_sold = self._calculate_max_input_token( input_token, qty, output_token ) @@ -693,54 +1060,456 @@ def _token_to_token_swap_output( func_params.insert(len(func_params) - 1, recipient) function = token_funcs.tokenToTokenTransferOutput(*func_params) return self._build_and_send_tx(function, tx_params) - else: - cost = self.get_token_token_output_price(input_token, output_token, qty) - amount_in_max = int((1 + self.max_slippage) * cost) + elif self.version == 2: + if recipient is None: + recipient = self.address + if route is None: + weth = self.get_weth_address() + path = ( + [input_token, output_token] + if is_same_address(input_token, weth) + or is_same_address(output_token, weth) + else [input_token, weth, output_token] + ) + else: + path = route + cost = self._get_token_token_output_price( + input_token, output_token, qty, fee=fee, route=path, fees=fees + ) + amount_in_max = int((1 + slippage) * cost) return self._build_and_send_tx( self.router.functions.swapTokensForExactTokens( qty, amount_in_max, - [input_token, self.get_weth_address(), output_token], - self.address, + path, + recipient, self._deadline(), ), ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + sqrtPriceLimitX96 = 0 + + if route and fees: + encoded_v3_path = self.encode_v3_path(route, fees, is_exact_out=True) + return self._build_and_send_tx( + self.router.functions.exactOutput( + { + "path": encoded_v3_path, + "recipient": recipient, + "deadline": self._deadline(), + "amountOut": qty, + "amountInMaximum": amount_in_max, + } + ), + self._get_tx_params(), + ) + else: + return self._build_and_send_tx( + self.router.functions.exactOutputSingle( + { + "tokenIn": input_token, + "tokenOut": output_token, + "fee": fee, + "recipient": recipient, + "deadline": self._deadline(), + "amountOut": qty, + "amountInMaximum": amount_in_max, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + }, + ), + self._get_tx_params(), + ) + else: + raise ValueError + + # ------ Wallet balance ------------------------------------------------------------ + def get_eth_balance(self) -> Wei: + """Get the balance of ETH for your address.""" + return self.w3.eth.get_balance(self.address) + + def get_token_balance(self, token: AddressLike) -> int: + """Get the balance of a token for your address.""" + _validate_address(token) + if _addr_to_str(token) == ETH_ADDRESS: + return self.get_eth_balance() + erc20 = _load_contract_erc20(self.w3, token) + balance: int = erc20.functions.balanceOf(self.address).call() + return balance + + # ------ ERC20 Pool ---------------------------------------------------------------- + @supports([1]) + def get_ex_eth_balance(self, token: AddressLike) -> int: + """Get the balance of ETH in an exchange contract.""" + ex_addr: AddressLike = self._exchange_address_from_token(token) + return self.w3.eth.get_balance(ex_addr) + + @supports([1]) + def get_ex_token_balance(self, token: AddressLike) -> int: + """Get the balance of a token in an exchange contract.""" + erc20 = _load_contract_erc20(self.w3, token) + balance: int = erc20.functions.balanceOf( + self._exchange_address_from_token(token) + ).call() + return balance + + # TODO: ADD TOTAL SUPPLY + @supports([1]) + def get_exchange_rate(self, token: AddressLike) -> float: + """Get the current ETH/token exchange rate of the token.""" + eth_reserve = self.get_ex_eth_balance(token) + token_reserve = self.get_ex_token_balance(token) + return float(token_reserve / eth_reserve) + + # ------ Liquidity ----------------------------------------------------------------- + @check_approval + @supports([1]) + def add_liquidity( + self, token: AddressLike, max_eth: Wei, min_liquidity: int = 1 + ) -> HexBytes: + """Add liquidity to the pool.""" + tx_params = self._get_tx_params(max_eth) + # Add 1 to avoid rounding errors, per + # https://hackmd.io/hthz9hXKQmSyXfMbPsut1g#Add-Liquidity-Calculations + max_token = int(max_eth * self.get_exchange_rate(token)) + 10 + func_params = [min_liquidity, max_token, self._deadline()] + function = self._exchange_contract(token).functions.addLiquidity(*func_params) + return self._build_and_send_tx(function, tx_params) + + @check_approval + @supports([1]) + def remove_liquidity(self, token: str, max_token: int) -> HexBytes: + """Remove liquidity from the pool.""" + func_params = [int(max_token), 1, 1, self._deadline()] + function = self._exchange_contract( + self.w3.to_checksum_address(token) + ).functions.removeLiquidity(*func_params) + return self._build_and_send_tx(function) + + @supports([3]) + def mint_liquidity( + self, + pool: Contract, + amount_0: int, + amount_1: int, + tick_lower: int, + tick_upper: int, + deadline: int = 2**64, + ) -> TxReceipt: + """ + add liquidity to pool and mint position nft + """ + + token_0 = pool.functions.token0().call() + token_1 = pool.functions.token1().call() + token_0_instance = _load_contract(self.w3, abi_name="erc20", address=token_0) + token_1_instance = _load_contract(self.w3, abi_name="erc20", address=token_1) + + balance_0 = self.get_token_balance(token_0) + balance_1 = self.get_token_balance(token_1) + + assert balance_0 > amount_0, f"Have {balance_0}, need {amount_0}: {token_0}" + assert balance_1 > amount_1, f"Have {balance_1}, need {amount_1}: {token_1}" + + fee = pool.functions.fee().call() + tick_lower = nearest_tick(tick_lower, fee) + tick_upper = nearest_tick(tick_upper, fee) + assert tick_lower < tick_upper, "Invalid tick range" + + *_, isInit = pool.functions.slot0().call() + # If pool is not initialized, init pool w/ sqrt_price_x96 encoded from amount_0 & amount_1 + if isInit is False: + sqrt_pricex96 = encode_sqrt_ratioX96(amount_0, amount_1) + pool.functions.initialize(sqrt_pricex96).transact( + {"from": _addr_to_str(self.address)} + ) + + nft_manager = self.nonFungiblePositionManager + token_0_instance.functions.approve(nft_manager.address, amount_0).transact( + {"from": _addr_to_str(self.address)} + ) + token_1_instance.functions.approve(nft_manager.address, amount_1).transact( + {"from": _addr_to_str(self.address)} + ) + + # TODO: add slippage param + tx_hash = nft_manager.functions.mint( + ( + token_0, + token_1, + fee, + tick_lower, + tick_upper, + amount_0, + amount_1, + 0, + 0, + self.address, + deadline, + ) + ).transact({"from": _addr_to_str(self.address)}) + receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) + return receipt + + # TODO: should this be multiple functions? + @supports([3]) + def close_position( + self, + tokenId: int, + amount0Min: int = 0, + amount1Min: int = 0, + deadline: int | None = None, + ) -> TxReceipt: + """ + remove all liquidity from the position associated w/ tokenId, collect fees, and burn token. + """ + position = self.nonFungiblePositionManager.functions.positions(tokenId).call() + + if deadline is None: + deadline = self._deadline() + + # If collecting fees in ETH, fees must be precomputed to protect against reentrancy + # source: https://docs.uniswap.org/sdk/guides/liquidity/removing + + if position[2] == WETH9_ADDRESS or position[3] == WETH9_ADDRESS: + amount0Min, amount1Min = self.nonFungiblePositionManager.functions.collect( + (tokenId, _addr_to_str(self.address), MAX_UINT_128, MAX_UINT_128) + ).call() + + tx_remove_liquidity = ( + self.nonFungiblePositionManager.functions.decreaseLiquidity( + (tokenId, position[7], amount0Min, amount1Min, deadline) + ).transact({"from": _addr_to_str(self.address)}) + ) + self.w3.eth.wait_for_transaction_receipt(tx_remove_liquidity) + + tx_collect_fees = self.nonFungiblePositionManager.functions.collect( + (tokenId, _addr_to_str(self.address), MAX_UINT_128, MAX_UINT_128) + ).transact({"from": _addr_to_str(self.address)}) + self.w3.eth.wait_for_transaction_receipt(tx_collect_fees) + + tx_burn = self.nonFungiblePositionManager.functions.burn(tokenId).transact( + {"from": _addr_to_str(self.address)} + ) + receipt = self.w3.eth.wait_for_transaction_receipt(tx_burn) + + return receipt + + # Below two functions derived from: https://stackoverflow.com/questions/71814845/how-to-calculate-uniswap-v3-pools-total-value-locked-tvl-on-chain + def get_token0_in_pool( + self, + liquidity: float, + sqrtPrice: float, + sqrtPriceLow: float, + sqrtPriceHigh: float, + ) -> float: + sqrtPrice = max(min(sqrtPrice, sqrtPriceHigh), sqrtPriceLow) + return liquidity * (sqrtPriceHigh - sqrtPrice) / (sqrtPrice * sqrtPriceHigh) + + def get_token1_in_pool( + self, + liquidity: float, + sqrtPrice: float, + sqrtPriceLow: float, + sqrtPriceHigh: float, + ) -> float: + sqrtPrice = max(min(sqrtPrice, sqrtPriceHigh), sqrtPriceLow) + return liquidity * (sqrtPrice - sqrtPriceLow) + + # Find maximum tick of the word at the largest index (wordPos) in the tickBitmap that contains an initialized tick + def get_max_tick_from_wordpos( + self, wordPos: int, bitmap: str, tick_spacing: int, fee: int + ) -> int: + compressed_tick = wordPos << 8 + _tick = compressed_tick * tick_spacing + min_tick_in_word = nearest_tick(_tick, fee) + max_tick_in_word = min_tick_in_word + (len(bitmap) * tick_spacing) + return max_tick_in_word + + # Find minimum tick of word at the smallest index (wordPos) in the tickBitmap that contains an initialized tick + def get_min_tick_from_wordpos( + self, wordPos: int, tick_spacing: int, fee: int + ) -> int: + compressed_tick = wordPos << 8 + _tick = compressed_tick * tick_spacing + min_tick_in_word = nearest_tick(_tick, fee) + return min_tick_in_word + + # Find min or max tick in initialized tick range using the tickBitmap + def find_tick_from_bitmap( + self, + bitmap_spacing: tuple[int, int], + pool: Contract, + tick_spacing: int, + fee: int, + left: bool = True, + ) -> int | bool: + # searching to the left (finding max tick) + if left: + min_wordPos = bitmap_spacing[1] + max_wordPos = bitmap_spacing[0] + step = -1 + # searching to the right (finding min tick) + else: + min_wordPos = bitmap_spacing[0] + max_wordPos = bitmap_spacing[1] + step = 1 + + # Some fun tickBitmap hacks below. + # Iterate thru each possible wordPos (based on tick_spacing), get the bitmap "word" (basically a sub-array of the full bitmap), + # check if there is an initialized tick, derive largest (or smallest) tick in this word + # + # Since wordPos (int16 index of tickBitmap mapping) are calculated by (tick/tickspacing) >> 8, deriving tick from wordPos + # is done by (wordPos << 8)*tickSpacing. This however does not find the precise tick (only a possible tick that could map to that bitmap sub-array, or word), + # thus we must calculate the nearest viable tick depending on the tick_spacing of the pool using nearest_tick(). + # If searching for the maximum tick, we must then add-back len(bitmap)*tick_spacing as each bit in the bitmap should correspond to a tick. + + for wordPos in range(min_wordPos, max_wordPos, step): + word = pool.functions.tickBitmap(wordPos).call() + bitmap = bin(word) + for bit in bitmap[3:]: + if int(bit) == 1: + if left: + _max_tick = self.get_max_tick_from_wordpos( + wordPos, bitmap, tick_spacing, fee + ) + return _max_tick + else: + _min_tick = self.get_min_tick_from_wordpos( + wordPos, tick_spacing, fee + ) + return _min_tick + return False + + def get_tvl_in_pool(self, pool: Contract) -> tuple[float, float]: + """ + Iterate through each tick in a pool and calculate the TVL on-chain + + Note: the output of this function may differ from what is returned by the + UniswapV3 subgraph api (https://github.com/Uniswap/v3-subgraph/issues/74) + + Params + ------ + pool: Contract + pool contract instance to find TVL + """ + pool_tick_output_types = ( + "uint128", + "int128", + "uint256", + "uint256", + "int56", + "uint160", + "uint32", + "bool", + ) + + pool_immutables = self.get_pool_immutables(pool) + pool_state = self.get_pool_state(pool) + fee = pool_immutables["fee"] + sqrtPrice = pool_state["sqrtPriceX96"] / (1 << 96) + + token0_liquidity = 0.0 + token1_liquidity = 0.0 + liquidity_total = 0.0 + + TICK_SPACING = _tick_spacing[fee] + BITMAP_SPACING = _tick_bitmap_range[fee] + + _max_tick = self.find_tick_from_bitmap( + BITMAP_SPACING, pool, TICK_SPACING, fee, True + ) + _min_tick = self.find_tick_from_bitmap( + BITMAP_SPACING, pool, TICK_SPACING, fee, False + ) + assert _max_tick is not False, "Error finding max tick" + assert _min_tick is not False, "Error finding min tick" + + Batch = namedtuple("Batch", "ticks batchResults") + ticks = [] + # Batching pool.functions.tick() calls as these are the major bottleneck to performance + for batch in list(chunks(range(_min_tick, _max_tick, TICK_SPACING), 100)): + _batch = [] + _ticks = [] + for tick in batch: + _batch.append( + ( + pool.address, + HexBytes(pool.functions.ticks(tick)._encode_transaction_data()), + ) + ) + _ticks.append(tick) + ticks.append(Batch(_ticks, self.multicall(_batch, pool_tick_output_types))) + + for tickBatch in ticks: + tick_arr = tickBatch.ticks + for i in range(len(tick_arr)): + tick = tick_arr[i] + tickData = tickBatch.batchResults[i] + # source: https://stackoverflow.com/questions/71814845/how-to-calculate-uniswap-v3-pools-total-value-locked-tvl-on-chain + liquidityNet = tickData[1] + liquidity_total += liquidityNet + sqrtPriceLow = 1.0001 ** (tick // 2) + sqrtPriceHigh = 1.0001 ** ((tick + TICK_SPACING) // 2) + token0_liquidity += self.get_token0_in_pool( + liquidity_total, sqrtPrice, sqrtPriceLow, sqrtPriceHigh + ) + token1_liquidity += self.get_token1_in_pool( + liquidity_total, sqrtPrice, sqrtPriceLow, sqrtPriceHigh + ) + + # Correcting for each token's respective decimals + token0_decimals = ( + _load_contract_erc20(self.w3, pool_immutables["token0"]) + .functions.decimals() + .call() + ) + token1_decimals = ( + _load_contract_erc20(self.w3, pool_immutables["token1"]) + .functions.decimals() + .call() + ) + token0_liquidity = token0_liquidity // (10**token0_decimals) + token1_liquidity = token1_liquidity // (10**token1_decimals) + return (token0_liquidity, token1_liquidity) # ------ Approval Utils ------------------------------------------------------------ - def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: + def approve(self, token: AddressLike, max_approval: int | None = None) -> TxReceipt: """Give an exchange/router max approval of a token.""" max_approval = self.max_approval_int if not max_approval else max_approval contract_addr = ( - self.exchange_address_from_token(token) + self._exchange_address_from_token(token) if self.version == 1 else self.router_address ) - function = self.erc20_contract(token).functions.approve( + function = _load_contract_erc20(self.w3, token).functions.approve( contract_addr, max_approval ) - logger.info(f"Approving {_addr_to_str(token)}...") + logger.warning(f"Approving {_addr_to_str(token)}...") tx = self._build_and_send_tx(function) - self.w3.eth.waitForTransactionReceipt(tx, timeout=6000) + receipt = 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) + return receipt def _is_approved(self, token: AddressLike) -> bool: """Check to see if the exchange and token is approved.""" _validate_address(token) if self.version == 1: - contract_addr = self.exchange_address_from_token(token) - else: + contract_addr = self._exchange_address_from_token(token) + elif self.version in [2, 3]: contract_addr = self.router_address + else: + raise ValueError amount = ( - self.erc20_contract(token) + _load_contract_erc20(self.w3, token) .functions.allowance(self.address, contract_addr) .call() ) - if amount >= self.max_approval_check_int: - return True - else: - return False + return bool(amount >= self.max_approval_check_int) # ------ Tx Utils ------------------------------------------------------------------ def _deadline(self) -> int: @@ -748,38 +1517,58 @@ def _deadline(self) -> int: return int(time.time()) + 10 * 60 def _build_and_send_tx( - self, function: ContractFunction, tx_params: Optional[TxParams] = None + self, function: ContractFunction, tx_params: TxParams | None = None ) -> HexBytes: """Build and send a transaction.""" if not tx_params: tx_params = self._get_tx_params() - transaction = function.buildTransaction(tx_params) + + # Pre-populate gas BEFORE build_transaction to prevent web3 from calling + # eth_estimateGas internally. web3 calls eth_estimateGas during + # build_transaction when no gas is provided, which fails for contracts + # with computed jumps (e.g. Vyper v1 exchange contracts) under Anvil's + # strict EVM. use_estimate_gas=True for networks like Arbitrum where 500k + # is not a safe default. + if "gas" not in tx_params and not self.use_estimate_gas: + tx_params["gas"] = Wei(500_000) + + transaction = function.build_transaction(tx_params) + + if "gas" not in tx_params: + # use_estimate_gas=True: run explicit estimate with 20% margin + # The Uniswap V3 UI uses 20% margin for transactions + transaction["gas"] = Wei(int(self.w3.eth.estimate_gas(transaction) * 1.2)) + signed_txn = self.w3.eth.account.sign_transaction( transaction, private_key=self.private_key ) # TODO: This needs to get more complicated if we want to support replacing a transaction # FIXME: This does not play nice if transactions are sent from other places using the same wallet. try: - return self.w3.eth.sendRawTransaction(signed_txn.rawTransaction) + return self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) finally: logger.debug(f"nonce: {tx_params['nonce']}") self.last_nonce = Nonce(tx_params["nonce"] + 1) - def _get_tx_params(self, value: Wei = Wei(0), gas: Wei = Wei(250000)) -> TxParams: + def _get_tx_params(self, value: Wei = Wei(0), gas: Wei | None = None) -> TxParams: """Get generic transaction parameters.""" - return { + params: TxParams = { "from": _addr_to_str(self.address), "value": value, - "gas": gas, "nonce": max( - self.last_nonce, self.w3.eth.getTransactionCount(self.address) + self.last_nonce, self.w3.eth.get_transaction_count(self.address) ), } + if gas: + params["gas"] = gas + + return params + # ------ Price Calculation Utils --------------------------------------------------- def _calculate_max_input_token( self, input_token: AddressLike, qty: int, output_token: AddressLike - ) -> Tuple[int, int]: + ) -> tuple[int, int]: """ For buy orders (exact output), the cost (input) is calculated. Calculate the max input and max eth sold for a token to token output swap. @@ -811,7 +1600,7 @@ def _calculate_max_input_token( def _calculate_max_output_token( self, output_token: AddressLike, qty: int, input_token: AddressLike - ) -> Tuple[int, int]: + ) -> tuple[int, int]: """ For sell orders (exact input), the amount bought (output) is calculated. Similar to _calculate_max_input_token, but for an exact input swap. @@ -838,49 +1627,495 @@ def _calculate_max_output_token( return int(outputAmountB), int(1.2 * outputAmountA) - # ------ Test utilities ------------------------------------------------------------ + # ------ Helpers ------------------------------------------------------------ - def _buy_test_assets(self) -> None: + # Batch contract function calls to speed up large on-chain data queries + def multicall( + self, + encoded_functions: Sequence[tuple[ChecksumAddress, bytes]], + output_types: Sequence[str], + ) -> list[Any]: """ - Buys some BAT and DAI. - Used in testing. + Calls aggregate() on Uniswap Multicall2 contract + + Params + ------ + encoded_functions : Sequence[Tuple[ChecksumAddress, bytes]] + array of tuples containing address of contract and byte-encoded transaction data + + output_types: Sequence[str] + array of solidity output types for decoding (e.g. uint256, bool, etc.) + + returns decoded results """ - ONE_ETH = 1 * 10 ** 18 - TEST_AMT = int(0.1 * ONE_ETH) - tokens = self._get_token_addresses() + params = [ + {"target": target, "callData": callData} + for target, callData in encoded_functions + ] + _, results = self.multicall2.functions.aggregate(params).call( + block_identifier="latest" + ) + decoded_results = [ + self.w3.codec.decode(output_types, multicall_result) + for multicall_result in results + ] + normalized_results = [ + map_abi_data(BASE_RETURN_NORMALIZERS, output_types, decoded_result) + for decoded_result in decoded_results + ] + return normalized_results + + def get_token(self, address: AddressLike, abi_name: str = "erc20") -> ERC20Token: + """ + Retrieves metadata from the ERC20 contract of a given token, like its name, symbol, and decimals. + """ + # FIXME: This function should always return the same output for the same input + # and would therefore benefit from caching + if address == "0x0000000000000000000000000000000000000000": + # This isn't exactly right, but for all intents and purposes, + # ETH is treated as a ERC20 by Uniswap. + return ERC20Token( + address=address, + name="ETH", + symbol="ETH", + decimals=18, + ) + token_contract = _load_contract(self.w3, abi_name, address=address) + try: + _name = token_contract.functions.name().call() + _symbol = token_contract.functions.symbol().call() + decimals = token_contract.functions.decimals().call() + except ValueError as e: + logger.warning( + f"Exception occurred while trying to get token {_addr_to_str(address)}: {e}" + ) + raise InvalidToken(address) + try: + name = _name.decode() + except (ValueError, AttributeError): + name = _name + try: + symbol = _symbol.decode() + except (ValueError, AttributeError): + symbol = _symbol + return ERC20Token(symbol, address, name, decimals) + + @supports([1, 2, 3]) + def get_weth_address(self) -> ChecksumAddress: + """Retrieves the WETH address from the contracts (which may vary between chains).""" + if hasattr(self, "_weth_address_cache"): + return self._weth_address_cache + if self.version == 1: + address: ChecksumAddress = self.w3.to_checksum_address(WETH9_ADDRESS) + elif self.version == 2: + # Contract calls should always return checksummed addresses + address = self.router.functions.WETH().call() + elif self.version == 3: + address = self.router.functions.WETH9().call() + else: + raise ValueError # pragma: no cover + + # Mainnet WETH9 address + # WETH9_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + # assert address == WETH9_ADDRESS, "WETH address mismatch" + + self._weth_address_cache: ChecksumAddress = address + return address + + @supports([3]) + def get_pool_instance( + self, token_0: AddressLike, token_1: AddressLike, fee: int = 3_000 + ) -> Contract: + """ + Returns an instance of a pool contract for a given token pair and fee. + Requires pair [token_in, token_out, fee] has a direct pool. + Will return 0x0 address if pool does not exist. + """ + + assert token_0 != token_1, "Token addresses cannot be the same" + fee = validate_fee_tier(fee=fee, version=self.version) + + pool_address = self.factory_contract.functions.getPool( + token_0, token_1, fee + ).call() + assert pool_address != ETH_ADDRESS, "0 address returned. Pool does not exist" + pool_instance = _load_contract( + self.w3, abi_name="uniswap-v3/pool", address=pool_address + ) + + return pool_instance + + @supports([3]) + def create_pool_instance( + self, token_0: AddressLike, token_1: AddressLike, fee: int = 3_000 + ) -> Contract: + """ + Creates and returns UniswapV3 Pool instance. Requires that fee is valid and no similar pool already exists. + """ + address = _addr_to_str(self.address) + assert token_0 != token_1, "Token addresses cannot be the same" + fee = validate_fee_tier(fee=fee, version=self.version) + + tx = self.factory_contract.functions.createPool(token_0, token_1, fee).transact( + {"from": address} + ) + receipt = self.w3.eth.wait_for_transaction_receipt(tx) + + event_logs = self.factory_contract.events.PoolCreated().process_receipt(receipt) + pool_address = event_logs[0]["args"]["pool"] + pool_instance = _load_contract( + self.w3, abi_name="uniswap-v3/pool", address=pool_address + ) + + return pool_instance + + @supports([3]) + def get_pool_immutables(self, pool: Contract) -> dict: + """ + Fetch on-chain pool data. + """ + pool_immutables = { + "factory": pool.functions.factory().call(), + "token0": pool.functions.token0().call(), + "token1": pool.functions.token1().call(), + "fee": pool.functions.fee().call(), + "tickSpacing": pool.functions.tickSpacing().call(), + "maxLiquidityPerTick": pool.functions.maxLiquidityPerTick().call(), + } + + return pool_immutables + + @supports([3]) + def get_pool_state(self, pool: Contract) -> dict: + """ + Fetch on-chain pool state. + """ + liquidity = pool.functions.liquidity().call() + slot = pool.functions.slot0().call() + pool_state = { + "liquidity": liquidity, + "sqrtPriceX96": slot[0], + "tick": slot[1], + "observationIndex": slot[2], + "observationCardinality": slot[3], + "observationCardinalityNext": slot[4], + "feeProtocol": slot[5], + "unlocked": slot[6], + } + + return pool_state + + @supports([3]) + def get_liquidity_positions(self) -> list[int]: + """ + Enumerates liquidity position tokens owned by address. + Returns array of token IDs. + """ + positions: list[int] = [] + number_of_positions = self.nonFungiblePositionManager.functions.balanceOf( + _addr_to_str(self.address) + ).call() + if number_of_positions > 0: + for idx in range(number_of_positions): + position = ( + self.nonFungiblePositionManager.functions.tokenOfOwnerByIndex( + _addr_to_str(self.address), idx + ).call() + ) + positions.append(position) + return positions + + # FIXME: mint call reverting - likely to do w/ passing struct args to contract function call + # FIXME: mint call reverting - likely due to handling of token amounts + + @supports([3]) + def mint_position(self, pool: Contract, amount0: int, amount1: int) -> HexBytes: + pool_immutables = self.get_pool_immutables(pool) + + token0 = pool_immutables["token0"] + token1 = pool_immutables["token1"] + fee = pool_immutables["fee"] - for token_name in ["BAT", "DAI"]: - token_addr = tokens[token_name.lower()] - price = self.get_eth_token_output_price(_str_to_addr(token_addr), TEST_AMT) - logger.info(f"Cost of {TEST_AMT} {token_name}: {price}") - logger.info("Buying...") - tx = self.make_trade_output( - tokens["eth"], tokens[token_name.lower()], TEST_AMT + positionManager = self.nonFungiblePositionManager + + approve0 = _load_contract_erc20(self.w3, token0).functions.approve( + self.positionManager_addr, amount0 + ) + logger.warning(f"Approving {_addr_to_str(token0)}...") + tx0 = self._build_and_send_tx(approve0) + self.w3.eth.wait_for_transaction_receipt(tx0, timeout=6000) + + approve1 = _load_contract_erc20(self.w3, token1).functions.approve( + self.positionManager_addr, amount1 * 1000 + ) + logger.warning(f"Approving {_addr_to_str(token1)}...") + tx1 = self._build_and_send_tx(approve1) + self.w3.eth.wait_for_transaction_receipt(tx1, timeout=6000) + + # tx_mint = pool.functions.mint(self.address, MIN_TICK, MAX_TICK, amount0,'').transact(); + + position = positionManager.encode_abi( + "mint", + args=[ + { + "token0": token0, + "token1": token1, + "fee": fee, + "tickLower": MIN_TICK, + "tickUpper": MAX_TICK, + "amount0Desired": amount0, + "amount1Desired": amount1, + "amount0Min": 0, + "amount1Min": 0, + "recipient": _addr_to_str(self.address), + "deadline": self._deadline(), + } + ], + ) + print(position) + + multicall = positionManager.functions.multicall([position]).transact( + {"from": _addr_to_str(self.address), "gas": Wei(417918)} + ) + + print(multicall) + # mint_position = positionManager.functions.mint({'token0':token0,'token1':token1,'fee':fee,'tickLower':MIN_TICK,'tickUpper':MAX_TICK, + # 'amount0Desired':amount0,'amount1Desired':amount1,'amount0Min':0,'amount1Min':0,'recipient':_addr_to_str(self.address),'deadline':self._deadline() + # }) + + # mint_tx = self._build_and_send_tx(mint_position) + # self.w3.eth.wait_for_transaction_receipt(mint_tx, timeout=6000) + # + # tx2 = self._build_and_send_tx(multicall,) + # self.w3.eth.wait_for_transaction_receipt(tx2, timeout=6000) + + # position = positionManager.functions.mint().buildTransaction() + # print(position['data']) + + return multicall + + @supports([3]) + def encode_v3_path( + self, route: list[AddressLike], fees: list[int], is_exact_out: bool = False + ) -> bytes: + """ + Encodes a Uniswap V3 path for use in swap and quote functions. + + :param route : A list of token addresses representing the swap route. The length of this list must be one greater than the length of the `fees` list. + :param fees : A list of fee tiers corresponding to each hop in the route. Each fee tier should be an integer representing the fee in hundredths of a bip (e.g., 3000 for 0.3%). + :param is_exact_out : A boolean indicating whether the swap is an exact output swap. If True, the path will be encoded in reverse order. + + :return : A bytes object representing the encoded path. + """ + if len(route) < 2: + raise ValueError( + "Route must contain at least two token addresses. " + f"Got route length {len(route)}." + ) + if len(route) != len(fees) + 1: + raise ValueError( + "Route length must be one greater than fees length. " + f"Got route length {len(route)} and fees length {len(fees)}." ) - self.w3.eth.waitForTransactionReceipt(tx) - def _get_token_addresses(self) -> Dict[str, str]: + path = b"" + _route = route.copy() + _fees = fees.copy() + if is_exact_out: + # For exact output swaps, the path is encoded in reverse order + _route.reverse() + _fees.reverse() + + for i in range(len(_fees)): + path += self.w3.to_bytes(hexstr=HexStr(_addr_to_str(_route[i]))) + _fees[ + i + ].to_bytes(3, "big") + path += self.w3.to_bytes(hexstr=HexStr(_addr_to_str(_route[-1]))) + + return path + + @supports([2, 3]) + def get_raw_price( + self, token_in: AddressLike, token_out: AddressLike, fee: int | None = None + ) -> float: """ - Returns a dict with addresses for tokens for the current net. - Used in testing. + Returns current price for pair of tokens [token_in, token_out] regrading liquidity that is being locked in the pool + Parameter `fee` is required for V3 only, can be omitted for V2 + Requires pair [token_in, token_out] having direct pool """ - netid = int(self.w3.net.version) - netname = _netid_to_name[netid] - if netname == "mainnet": - return { - "eth": "0x0000000000000000000000000000000000000000", - "bat": Web3.toChecksumAddress( - "0x0D8775F648430679A709E98d2b0Cb6250d2887EF" - ), - "dai": Web3.toChecksumAddress( - "0x6b175474e89094c44da98b954eedeac495271d0f" - ), - } - elif netname == "rinkeby": - return { - "eth": "0x0000000000000000000000000000000000000000", - "bat": "0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B", - "dai": "0x2448eE2641d78CC42D7AD76498917359D961A783", - } + fee = validate_fee_tier(fee=fee, version=self.version) + + if token_in == ETH_ADDRESS: + token_in = self.get_weth_address() + if token_out == ETH_ADDRESS: + token_out = self.get_weth_address() + + if self.version == 2: + params: Iterable[ChecksumAddress | int | None] = [ + self.w3.to_checksum_address(token_in), + self.w3.to_checksum_address(token_out), + ] + pair_token = self.factory_contract.functions.getPair(*params).call() + token_in_erc20 = _load_contract_erc20( + self.w3, self.w3.to_checksum_address(token_in) + ) + token_in_balance = int( + token_in_erc20.functions.balanceOf( + self.w3.to_checksum_address(pair_token) + ).call() + ) + token_in_decimals = self.get_token(token_in).decimals + token_in_balance = token_in_balance / (10**token_in_decimals) + + token_out_erc20 = _load_contract_erc20( + self.w3, self.w3.to_checksum_address(token_out) + ) + token_out_balance = int( + token_out_erc20.functions.balanceOf( + self.w3.to_checksum_address(pair_token) + ).call() + ) + token_out_decimals = self.get_token(token_out).decimals + token_out_balance = token_out_balance / (10**token_out_decimals) + + raw_price = token_out_balance / token_in_balance else: - raise Exception(f"Unknown net '{netname}'") + params = [ + self.w3.to_checksum_address(token_in), + self.w3.to_checksum_address(token_out), + fee, + ] + pool_address = self.factory_contract.functions.getPool(*params).call() + pool_contract = _load_contract( + self.w3, abi_name="uniswap-v3/pool", address=pool_address + ) + # t0 = pool_contract.functions.token0().call() + t1 = pool_contract.functions.token1().call() + if t1.lower() == token_in.lower(): + den0 = self.get_token(token_in).decimals + den1 = self.get_token(token_out).decimals + else: + den0 = self.get_token(token_out).decimals + den1 = self.get_token(token_in).decimals + sqrtPriceX96 = pool_contract.functions.slot0().call()[0] + raw_price = (sqrtPriceX96 * sqrtPriceX96 * 10**den1 >> (96 * 2)) / ( + 10**den0 + ) + if t1.lower() == token_in.lower(): + raw_price = 1 / raw_price + return raw_price + + def estimate_price_impact( + self, + token_in: AddressLike, + token_out: AddressLike, + amount_in: int, + fee: int, + route: list[AddressLike] | None = None, + ) -> float: + """ + Returns the estimated price impact as a positive float (0.01 = 1%). + + NOTE: Work-in-progress. + + See ``examples/price_impact.py`` for an example which uses this. + """ + try: + price_small = self.get_raw_price( + token_in, + token_out, + fee=fee, + ) + except (ArithmeticError, BadFunctionCallOutput): + # ArithmeticError is raised when `token_in` amount in the pool equals 0. + # BadFunctionCallOutput is raised when the pool's contract for given `(token_in, token_out, fee)` hasn't been deployed + return 1 + + if price_small == 0: + # Occurs when `token_out` amount in the pool equals 0 + return 1 + try: + cost_amount = self.get_price_input( + token_in, token_out, amount_in, fee=fee, route=route + ) + except ContractLogicError: + # ContractLogicError is raised when the pool's contract for given `(token_in, token_out, fee)` hasn't been deployed. + # As `get_price_input()` uses UniswapV3Quoter for getting prices, that contract raises such exception in this situation. + return 1 + price_amount = ( + cost_amount / (amount_in / (10 ** self.get_token(token_in).decimals)) + ) / 10 ** self.get_token(token_out).decimals + + # 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]) + def get_fee_maker(self) -> float: + """Get the maker fee.""" + return 0 + + @supports([1, 2]) + def get_fee_taker(self) -> float: + """Get the taker fee.""" + return 0.003 + + # ---- Old v1 utils ---- + + @supports([1]) + def _exchange_address_from_token(self, token_addr: AddressLike) -> AddressLike: + ex_addr: AddressLike = self.factory_contract.functions.getExchange( + token_addr + ).call() + # TODO: What happens if the token doesn't have an exchange/doesn't exist? + # Should probably raise an Exception (and test it) + return ex_addr + + @supports([1]) + def _token_address_from_exchange(self, exchange_addr: AddressLike) -> Address: + token_addr: Address = ( + self._exchange_contract(ex_addr=exchange_addr) + .functions.tokenAddress(exchange_addr) + .call() + ) + return token_addr + + @supports([1]) + def _exchange_contract( + self, + token_addr: AddressLike | None = None, + ex_addr: AddressLike | None = None, + ) -> Contract: + if not ex_addr and token_addr: + ex_addr = self._exchange_address_from_token(token_addr) + if ex_addr is None: + raise InvalidToken(token_addr) + abi_name = "uniswap-v1/exchange" + contract = _load_contract(self.w3, abi_name=abi_name, address=ex_addr) + logger.info(f"Loaded exchange contract {contract} at {contract.address}") + return contract + + @supports([1]) + def _get_all_tokens(self) -> list[ERC20Token]: + """ + Retrieves all token pairs. + + Note: This is a *very* expensive operation and might therefore not work properly. + """ + # FIXME: This is a very expensive operation, would benefit greatly from caching. + tokenCount = self.factory_contract.functions.tokenCount().call() + tokens = [] + for i in range(tokenCount): + address = self.factory_contract.functions.getTokenWithId(i).call() + if address == "0x0000000000000000000000000000000000000000": + # Token is ETH + continue + token = self.get_token(address) + tokens.append(token) + return tokens diff --git a/uniswap/uniswap4.py b/uniswap/uniswap4.py new file mode 100644 index 00000000..78fc413f --- /dev/null +++ b/uniswap/uniswap4.py @@ -0,0 +1,3436 @@ +import logging +import os +import time +from dataclasses import astuple +from decimal import Decimal + +import eth_abi.abi +from eth_abi import encode +from eth_abi.packed import encode_packed +from web3 import Web3 +from web3.contract import Contract +from web3.contract.contract import ContractFunction +from web3.exceptions import BadFunctionCallOutput, ContractLogicError, NameNotFound +from web3.types import ( + HexBytes, + Nonce, + TxParams, + Wei, +) + +from .constants import ( + ETH_ADDRESS, + Q96, + ZERO_HOOK, + _netid_to_name, + _permit2_contract_addresses_v4, + _poolmanager_contract_addresses_v4, + _position_descriptor_contract_addresses_v4, + _position_manager_contract_addresses_v4, + _quoter_contract_addresses_v4, + _reserves_lens_contract_addresses_v4, + _router_contract_addresses_v4, + _stateview_contract_addresses_v4, + universal_router_commands, + universal_router_commands_abis, + v4_actions, + v4_actions_abis, +) +from .exceptions import InvalidToken +from .token import ERC20Token +from .types import ( + AddressLike, + ModifyLiquidityParams, + PathKey, + PermitBatch, + PermitSingle, + PoolKey, + SwapParams, +) +from .util import ( + _addr_to_str, + _load_abi, + _load_contract, + _str_to_addr, + get_sqrt_ratio_at_tick, + realised_fee_percentage, +) + +logger = logging.getLogger(__name__) + + +class Uniswap4: + """ + Wrapper around Uniswap v4 contracts. + """ + + w3: Web3 + address: AddressLike + last_nonce: Nonce + + def __init__( + self, + address: str | AddressLike, + private_key: str | None = None, + provider: str | None = None, + web3: Web3 | None = None, + max_slippage: float = 0.01, + gas_limit: float = 250000.0, + gas_price: float = 1.80, + priority_fee: float = 1.0, + post_merge: bool = True, + ) -> None: + """ + :param address: The public address of the ETH wallet to use. + :param private_key: The private key of the ETH wallet to use. + :param provider: Can be optionally set to a Web3 provider URI. If none set, will fall back to the PROVIDER environment variable, or web3 if set. + :param web3: Can be optionally set to a custom Web3 instance. + :param max_slippage: Maximum slippage for a trade, as a float (0.01 is 1%). + :param gas_limit: Maximum gas amount allocated for transactions. + :param gas_price: Cost per unit of gas, in GWei. + :param priority_fee: Amount of ETH to pay to the block producers, in GWei. Affects tx position in the block, the bigger value, the higher position is. + :param post_merge: True is for post-Merge transations, False for legacy ones. + """ + + self.address: AddressLike = ( + _str_to_addr(address) if isinstance(address, str) else address + ) + self.private_key = private_key + + self.max_slippage = max_slippage + + if web3: + self.w3 = web3 + else: + self.provider = provider or os.environ["PROVIDER"] + self.w3 = Web3( + Web3.HTTPProvider(self.provider, request_kwargs={"timeout": 60}) + ) + + self.last_nonce = self.w3.eth.get_transaction_count(self.address) + + # This code automatically approves you for trading on the exchange. + # max_approval is to allow the contract to exchange on your behalf. + # max_approval_check checks that current approval is above a reasonable + # number + # The program cannot check for max_approval each time because it + # decreases with each trade. + self.max_approval_hex = f"0x{64 * 'f'}" + self.max_approval_int = int(self.max_approval_hex, 16) + self.max_approval_check_hex = f"0x{15 * '0'}{49 * 'f'}" + self.max_approval_check_int = int(self.max_approval_check_hex, 16) + self.gas_limit = gas_limit + self.gas_price = gas_price + self.post_merge = post_merge + self.priority_fee = priority_fee + + chain_id = int(self.w3.net.version) + self.net_id = chain_id + if self.net_id in _netid_to_name: + self.net_name = _netid_to_name[self.net_id] + if self.net_name not in _quoter_contract_addresses_v4: + raise ValueError(f"Netid {self.net_id} is not supported yet.") + else: + raise ValueError(f"Unknown netid: {self.net_id}") + + logger.info(f"Using {self.w3} ('{self.net_name}', netid: {self.net_id})") + quoter_address = _quoter_contract_addresses_v4[self.net_name] + router_address = _router_contract_addresses_v4[self.net_name] + stateview_address = _stateview_contract_addresses_v4[self.net_name] + permit2_address = _permit2_contract_addresses_v4[self.net_name] + position_descriptor_address = _position_descriptor_contract_addresses_v4[ + self.net_name + ] + pool_manager_address = _poolmanager_contract_addresses_v4[self.net_name] + position_manager_address = _position_manager_contract_addresses_v4[ + self.net_name + ] + reserves_lens_address = _reserves_lens_contract_addresses_v4[self.net_name] + + """ + NOTE: Following exception handling relates to the testnets only, as production contracts are always deployed. + Specifically, PositionDescriptor is not deployed on Sepolia and Unichain Sepolia yet, so loading it will raise an error. + However, this is a periphery helper which is not critical for core functionality. + """ + try: + self.quoter_address = _str_to_addr(quoter_address) + self.quoter = _load_contract( + self.w3, abi_name="uniswap-v4/quoter", address=self.quoter_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading quoter contract: {e}") + + try: + self.router_address = _str_to_addr(router_address) + self.router = _load_contract( + self.w3, abi_name="uniswap-v4/router", address=self.router_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading router contract: {e}") + + try: + self.stateview_address = _str_to_addr(stateview_address) + self.stateview = _load_contract( + self.w3, abi_name="uniswap-v4/stateview", address=self.stateview_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading stateview contract: {e}") + + try: + self.permit2_address = _str_to_addr(permit2_address) + self.permit2 = _load_contract( + self.w3, abi_name="uniswap-v4/permit2", address=self.permit2_address + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading permit2 contract: {e}") + + try: + self.position_descriptor_address = _str_to_addr(position_descriptor_address) + self.position_descriptor = _load_contract( + self.w3, + abi_name="uniswap-v4/pos_descriptor", + address=self.position_descriptor_address, + ) + except NameNotFound as e: + logger.error( + f"Error occurred while loading position descriptor contract: {e}" + ) + + try: + self.pool_manager_address = _str_to_addr(pool_manager_address) + self.pool_manager = _load_contract( + self.w3, + abi_name="uniswap-v4/poolmanager", + address=self.pool_manager_address, + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading pool manager contract: {e}") + + try: + self.position_manager_address = _str_to_addr(position_manager_address) + self.position_manager = _load_contract( + self.w3, + abi_name="uniswap-v4/pos_manager", + address=self.position_manager_address, + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading position manager contract: {e}") + + try: + self.reserves_lens_address = _str_to_addr(reserves_lens_address) + self.reserves_lens = _load_contract( + self.w3, + abi_name="uniswap-v4/reserves_lens", + address=self.reserves_lens_address, + ) + except NameNotFound as e: + logger.error(f"Error occurred while loading reserves lens contract: {e}") + + # Approvals + def approve( + self, + token: AddressLike, + max_approval: int | None = None, + delay_interval: int | None = 7, + approve_position_manager: bool = False, + ) -> HexBytes: + """Approve the router to spend a token on the user's behalf, or set up a permit for the position manager to pull the token from the user's wallet. For ETH, the router can pull from the user's wallet directly, so no approval is necessary. + + :param token: The address of the token to approve. + :param max_approval: Optional. The maximum amount to approve. If not set, will approve a maximum possible amount. + :param delay_interval: Optional. Seconds to wait between two approval transactions. Defaults to 7. Values less than 1 are treated as default. + :param approve_position_manager: Optional. Whether to approve the position manager to spend the token. Defaults to False. + """ + + # If the token is not ETH, approve the router to spend it. For ETH, the router can pull from the user's wallet directly, so no approval is necessary. + if _addr_to_str(token) != ETH_ADDRESS: + max_approval = self.max_approval_int if not max_approval else max_approval + function = self.erc20_contract(token).functions.approve( + _addr_to_str(self.permit2_address), max_approval + ) + logger.info(f"Approving {_addr_to_str(token)} for PERMIT2...") + tx = self._build_and_send_tx(function) + if delay_interval is None or delay_interval < 1: + delay_interval = 7 + time.sleep(delay_interval) + else: + raise ValueError("ETH needs no approval.") + # Give an exchange/router max approval for a token. + max_approval = 2**100 - 1 + expiration: int = 10**12 + logger.info(f"Setting permit for {_addr_to_str(token)} at router contract...") + function = self.permit2.functions.approve( + _str_to_addr(token), self.router_address, max_approval, expiration + ) + tx = self._build_and_send_tx(function) + + if approve_position_manager: + # time.sleep(delay_interval) + # max_approval = self.max_approval_int + # function = self.erc20_contract(token).functions.approve( + # _addr_to_str(self.position_manager_address), max_approval + # ) + # logger.info(f"Approving {_addr_to_str(token)} for PositionManager...") + # tx = self._build_and_send_tx(function) + + time.sleep(delay_interval) + max_approval = 2**100 - 1 + expiration = 10**12 + logger.info( + f"Setting permit for {_addr_to_str(token)} at position manager contract..." + ) + function = self.permit2.functions.approve( + _str_to_addr(token), + self.position_manager_address, + max_approval, + expiration, + ) + tx = self._build_and_send_tx(function) + return tx + + def approval(self, token: AddressLike) -> int: + """Returns the current allowance for the router to spend a token on the user's behalf. Note that this is not the allowance of the token itself, but the allowance set in the Permit2 contract for the router to spend the token.""" + # [0]=current allowance, [1]=allowance expiration [2]=current nonce + result = int( + self.permit2.functions.allowance( + self.address, token, self.router.address + ).call()[0] + ) + return result + + # Transaction parameters customization + # Gas limit + def get_gas_limit(self) -> float: + """Returns the current gas limit for transactions.""" + return self.gas_limit + + def set_gas_limit(self, gas_limit: float) -> None: + """Sets the gas limit for transactions.""" + self.gas_limit = gas_limit + + # Gas price in GWei + def get_gas_price(self) -> float: + """Returns the current gas price in GWei.""" + return self.gas_price + + def set_gas_price(self, gas_price: float) -> None: + """Sets the gas price in GWei.""" + self.gas_price = gas_price + + # Priority fee in GWei + def get_gas_priorityfee(self) -> float: + """Returns the current priority fee in GWei.""" + return self.priority_fee + + def set_gas_priorityfee(self, priority_fee: float) -> None: + """Sets the priority fee in GWei.""" + self.priority_fee = priority_fee + + # Slippage + def get_max_slippage(self) -> float: + """Returns the current maximum slippage as a float (0.01 is 1%).""" + return self.max_slippage + + def set_max_slippage(self, max_slippage: float) -> None: + """Sets the maximum slippage as a float (0.01 is 1%).""" + self.max_slippage = max_slippage + + # Nonce management + def update_last_nonce(self) -> None: + """Updates the last nonce to the current nonce of the wallet. This can be used to resync the nonce if transactions have been sent outside of this class or custom nonce is used.""" + self.last_nonce = self.w3.eth.get_transaction_count(self.address) + + # StateView methods + def stateview_get_fee_growth_globals( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ) -> dict: + """ + Retrieves the global fee growth of a pool. + """ + if token0.lower() > token1.lower(): + token0, token1 = token1, token0 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + fee_growth_globals: dict = self.stateview.functions.getFeeGrowthGlobals( + pool_id + ).call() + return_value = { + "feeGrowthGlobal0": fee_growth_globals[0], + "feeGrowthGlobal1": fee_growth_globals[1], + } + return return_value + + def stateview_get_fee_growth_inside( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick_lower: int, + tick_upper: int, + ) -> dict: + """ + Calculates the fee growth inside a tick range of a pool + """ + if token0.lower() > token1.lower(): + token0, token1 = token1, token0 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + fee_growth_inside: dict = self.stateview.functions.getFeeGrowthInside( + pool_id, tick_lower, tick_upper + ).call() + return_value = { + "feeGrowthInside0X128": fee_growth_inside[0], + "feeGrowthInside1X128": fee_growth_inside[1], + } + return return_value + + def stateview_get_liquidity( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ) -> int: + """Retrieves the total liquidity of a pool.""" + if token0.lower() > token1.lower(): + token0, token1 = token1, token0 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + liquidity: int = self.stateview.functions.getLiquidity(pool_id).call() + return liquidity + + def stateview_get_position_info( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + owner: str, + tick_lower: int, + tick_upper: int, + token_id: int, + ) -> dict: + """ + Retrieves position info in a pool. + + :param token_id: TokenID of the correspoding NFT + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + salt = HexBytes(token_id.to_bytes(32, byteorder="big")) + position_info: dict = self.stateview.functions.getPositionInfo( + pool_id, owner, tick_lower, tick_upper, salt + ).call() + return_value = { + "liquidity": position_info[0], + "feeGrowthInside0LastX128": position_info[1], + "feeGrowthInside1LastX128": position_info[2], + } + return return_value + + def stateview_get_slot0( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + ) -> dict: + """ + Returns current state of the pool. + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + slot: dict = self.stateview.functions.getSlot0(pool_id).call() + return_value = { + "sqrtPriceX96": slot[0], + "tick": slot[1], + "protocolFee": slot[2], + "lpFee": slot[3], + } + return return_value + + def stateview_get_tick_bitmap( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick: int, + ) -> int: + """ + Retrieves the tick bitmap of a pool at a specific tick. + + :param tick: MUST be int16 + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + + tick_bitmap: int = self.stateview.functions.getTickBitmap(pool_id, tick).call() + return tick_bitmap + + def stateview_get_tick_fee_growth_outside( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick: int, + ) -> dict: + """ + Retrieves the fee growth outside a tick range of a pool + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + fee_growth_outside: dict = self.stateview.functions.getTickFeeGrowthOutside( + pool_id, tick + ).call() + return_value = { + "feeGrowthOutside0X128": fee_growth_outside[0], + "feeGrowthOutside1X128": fee_growth_outside[1], + } + return return_value + + def stateview_get_tick_pool_info( + self, + token0: str, + token1: str, + fee: int, + tick_spacing: int, + hooks: str, + tick: int, + ) -> dict: + """ + Retrieves the tick information of a pool at a specific tick. + """ + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + pool = PoolKey(token0, token1, fee, tick_spacing, hooks) + pool_id = self.get_pool_id(pool) + tick_info: dict = self.stateview.functions.getTickInfo(pool_id, tick).call() + return_value = { + "liquidityGross": tick_info[0], + "liquidityNet": tick_info[1], + "feeGrowthOutside0X128": tick_info[2], + "feeGrowthOutside1X128": tick_info[3], + } + return return_value + + # ReservesLens methods + def reserves_lens_get_pool_tvl( + self, pool_key: PoolKey, custom_provider: str = "" + ) -> dict: + """ + Retrieves the total value locked (TVL) of a pool. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_key: The PoolKey object representing the pool. + :param custom_provider: The custom provider address, empty string for default. + :returns: A dictionary containing the reserves of the pool. + """ + if custom_provider == "": + reserves: dict = self.reserves_lens.functions.getPoolTVL( + _addr_to_str(self.pool_manager_address), astuple(pool_key) + ).call() + else: + reserves = self.reserves_lens.functions.getPoolTVL( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + custom_provider, + ).call() + return_value = { + "coreAmount0": reserves[0], + "coreAmount1": reserves[1], + "hookReserves0": reserves[2], + "hookReserves1": reserves[3], + "hookEffective0": reserves[4], + "hookEffective1": reserves[5], + "sqrtPriceX96": reserves[6], + "tick": reserves[7], + "activeLiquidity": reserves[8], + "blockNumber": reserves[9], + "statsProvider": reserves[10], + "hookPermissions": reserves[11], + "hasCustomAccounting": reserves[12], + "statsStatus": reserves[13], + } + return return_value + + def reserves_lens_get_pool_tvl_batch( + self, pool_keys: list[PoolKey], custom_provider: list[str] | None = None + ) -> list[dict]: + """ + Retrieves the total value locked (TVL) of multiple pools in a batch. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_keys: A list of PoolKey objects representing the pools. + :param custom_provider: The custom provider addresses for each pool. If None, will use default for all pools. + :returns: A list of dictionaries containing the reserves of each pool. + """ + if custom_provider is None: + custom_provider = [""] * len(pool_keys) + + reserves_list: list = self.reserves_lens.functions.getPoolTVLBatch( + _addr_to_str(self.pool_manager_address), + [astuple(pool_key) for pool_key in pool_keys], + ).call() + else: + reserves_list = self.reserves_lens.functions.getPoolTVLBatch( + _addr_to_str(self.pool_manager_address), + [astuple(pool_key) for pool_key in pool_keys], + custom_provider, + ).call() + + return_value = [] + for reserves in reserves_list: + return_value.append( + { + "coreAmount0": reserves[0], + "coreAmount1": reserves[1], + "hookReserves0": reserves[2], + "hookReserves1": reserves[3], + "hookEffective0": reserves[4], + "hookEffective1": reserves[5], + "sqrtPriceX96": reserves[6], + "tick": reserves[7], + "activeLiquidity": reserves[8], + "blockNumber": reserves[9], + "statsProvider": reserves[10], + "hookPermissions": reserves[11], + "hasCustomAccounting": reserves[12], + "statsStatus": reserves[13], + } + ) + + return return_value + + def reserves_lens_get_tvl_paged( + self, + pool_key: PoolKey, + cursor: bytes, + custom_provider: str = "", + max_reads: int = 0, + ) -> tuple[dict, bytes, bool]: + """ + Retrieves the total value locked (TVL) of multiple pools in a paged manner. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_key: A list of PoolKey objects representing the pools. + :param cursor: The cursor for paged retrieval. + :param custom_provider: The custom provider address, empty string for default. + :param max_reads: The maximum number of reads to perform using custom provider. + :returns: A tuple containing the reserves dictionary, the next cursor, and a boolean indicating if the paged retrieval is done. + """ + if custom_provider == "": + reserves_tuple, next_cursor, done = ( + self.reserves_lens.functions.getPoolTVLPaged( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + cursor, + ).call() + ) + else: + reserves_tuple, next_cursor, done = ( + self.reserves_lens.functions.getPoolTVLPaged( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + custom_provider, + cursor, + max_reads, + ).call() + ) + + reserves: dict = { + "coreAmount0": reserves_tuple[0], + "coreAmount1": reserves_tuple[1], + "hookReserves0": reserves_tuple[2], + "hookReserves1": reserves_tuple[3], + "hookEffective0": reserves_tuple[4], + "hookEffective1": reserves_tuple[5], + "sqrtPriceX96": reserves_tuple[6], + "tick": reserves_tuple[7], + "activeLiquidity": reserves_tuple[8], + "blockNumber": reserves_tuple[9], + "statsProvider": reserves_tuple[10], + "hookPermissions": reserves_tuple[11], + "hasCustomAccounting": reserves_tuple[12], + "statsStatus": reserves_tuple[13], + } + + return_value = (reserves, next_cursor, done) + return return_value + + def reserves_lens_get_populated_ticks_in_word( + self, pool_key: PoolKey, word_position: int + ) -> list[dict]: + """ + Retrieves the populated ticks in a specific word of a pool. + See https://github.com/Uniswap/v4-periphery/blob/main/src/interfaces/IReservesLens.sol for more details. + + :param pool_key: The PoolKey object representing the pool. + :param word_position: The position of the word to retrieve. + :returns: A list of dictionaries containing the populated ticks. + """ + results: list = self.reserves_lens.functions.getPopulatedTicksInWord( + _addr_to_str(self.pool_manager_address), + astuple(pool_key), + word_position, + ).call() + return_value: list[dict] = [] + for result in results: + return_value.append( + { + "tick": result[0], + "liquidityNet": result[1], + "liquidityGross": result[2], + } + ) + return return_value + + # PositionDescriptor methods + def position_descriptor_get_currency_ratio_priority(self, currency: str) -> int: + """ + For certain currencies on mainnet, the smaller the currency, the higher the priority. + And those with the higher priority values (more positive values) will be in the numerator of the price ratio + + :returns: The priority of a currency. + """ + ratio_priority: int = int( + self.position_descriptor.functions.currencyRatioPriority(currency).call() + ) + return_value = ratio_priority + return return_value + + def position_descriptor_get_flip_ratio( + self, currency0: str, currency1: str + ) -> bool: + """ + :returns: True if currency0 has higher priority than currency1 + """ + flip_ratio: bool = bool( + self.position_descriptor.functions.flipRatio(currency0, currency1).call() + ) + return_value = flip_ratio + return return_value + + def position_descriptor_get_native_currency_label(self) -> str: + """ + :returns: The label for the native currency as a string + """ + native_currency_label: str = str( + self.position_descriptor.functions.nativeCurrencyLabel().call() + ) + return_value = native_currency_label + return return_value + + def position_descriptor_get_pool_manager(self) -> str: + """ + :returns: PoolManager address as a string + """ + pool_manager: str = str(self.position_descriptor.functions.poolManager().call()) + return_value = pool_manager + return return_value + + def position_descriptor_get_token_uri(self, pos_manager: str, token_id: int) -> str: + """ + Produces the URI describing a particular token ID + Note this URI may be a data: URI with the JSON contents directly inlined + + :returns: The URI of the ERC721-compliant metadata + """ + token_uri: str = str( + self.position_descriptor.functions.tokenURI(pos_manager, token_id).call() + ) + return_value = token_uri + return return_value + + def position_descriptor_get_wrapped_native_address(self) -> str: + """ + :returns: The wrapped native currency address as a string + """ + wrapped_native_address: str = str( + self.position_descriptor.functions.wrappedNative().call() + ) + return_value = wrapped_native_address + return return_value + + # PositionManager methods + # Read methods + def position_manager_get_domain_separator( + self, + ) -> bytes: + """ + :returns: The domain separator for the current chain; bytes32 + """ + domain_separator: bytes = bytes( + self.position_manager.functions.DOMAIN_SEPARATOR().call() + ) + return_value = domain_separator + return return_value + + def position_manager_get_weth9( + self, + ) -> str: + """ + :returns: The wrapped native token address + """ + weth9: str = str(self.position_manager.functions.WETH9().call()) + return_value = weth9 + return return_value + + def position_manager_get_balance_of(self, address: str) -> int: + """ + :returns: The number of tokens in owner's address. + """ + balance: int = int(self.position_manager.functions.balanceOf(address).call()) + return_value = balance + return return_value + + def position_manager_get_approved(self, token_id: int) -> str: + """ + :returns: The account approved for a token. + """ + operator: str = str( + self.position_manager.functions.getApproved(token_id).call() + ) + return_value = operator + return return_value + + def position_manager_get_pool_and_position_info(self, token_id: int) -> dict: + """ + :returns: The PoolKey class object and position info of a position + """ + pool_key_tuple, info = self.position_manager.functions.getPoolAndPositionInfo( + token_id + ).call() + pool_key: PoolKey = PoolKey(*pool_key_tuple) + return_value = { + "poolKey": pool_key, + "info": info, + } + return return_value + + def position_manager_get_position_liquidity(self, token_id: int) -> int: + """ + :returns: The liquidity of a position + """ + position_liquidity: int = int( + self.position_manager.functions.getPositionLiquidity(token_id).call() + ) + return_value = position_liquidity + return return_value + + def position_manager_get_is_approved_for_all( + self, owner: str, operator: str + ) -> bool: + """ + :returns: True if the operator is allowed to manage all of the assets of owner + """ + is_approved_for_all: bool = bool( + self.position_manager.functions.isApprovedForAll(owner, operator).call() + ) + return_value = is_approved_for_all + return return_value + + def position_manager_get_msg_sender( + self, + ) -> str: + """ + :returns: address considered executor of the actions + + The other context functions, _msgData and _msgValue, are not supported by this contract. + In many contracts this will be the address that calls the initial entry point + that calls `_executeActions` `msg.sender` shouldn't be used, as this will be + the v4 pool manager contract that calls `unlockCallback` + If using ReentrancyLock.sol, this function can return _getLocker() + """ + msg_sender: str = str(self.position_manager.functions.msgSender().call()) + return_value = msg_sender + return return_value + + def position_manager_get_name( + self, + ) -> str: + """ + :returns: The name of the PositionManager token + """ + name: str = str(self.position_manager.functions.name().call()) + return_value = name + return return_value + + def position_manager_get_next_token_id( + self, + ) -> int: + """ + :returns: The ID that will be used for the next minted liquidity position + """ + next_token_id: int = int(self.position_manager.functions.nextTokenId().call()) + return_value = next_token_id + return return_value + + def position_manager_get_nonces(self, owner: str, word: int) -> int: + """ + :returns: Mapping of nonces consumed by each address, where a nonce is a single bit on the 256-bit bitmap + """ + bitmap: int = int(self.position_manager.functions.nonces(owner, word).call()) + return_value = bitmap + return return_value + + def position_manager_get_owner_of(self, token_id: int) -> str: + """ + :returns: The owner of the position for a given token ID + """ + owner: str = str(self.position_manager.functions.ownerOf(token_id).call()) + return_value = owner + return return_value + + def position_manager_get_permit2( + self, + ) -> str: + """ + :returns: The Permit2 contract to forward approvals + """ + permit2: str = str(self.position_manager.functions.permit2().call()) + return_value = permit2 + return return_value + + def position_manager_get_pool_keys(self, pool_id_trunc: bytes) -> PoolKey: + """ + :param pool_id_trunc: The truncated ID of the pool, first 25 bytes of common pool_id + :returns: The PoolKey class object for a given token ID + """ + pool_keys_tuple = self.position_manager.functions.poolKeys(pool_id_trunc).call() + pool_keys: PoolKey = PoolKey(*pool_keys_tuple) + return_value = pool_keys + return return_value + + def position_manager_get_position_info(self, token_id: int) -> int: + """ + :returns: The position info for a given token ID + """ + position_info: int = int( + self.position_manager.functions.positionInfo(token_id).call() + ) + return_value = position_info + return return_value + + def position_manager_get_subscriber(self, token_id: int) -> str: + """ + :returns: The subscriber of the position for a given token ID + """ + subscriber: str = str( + self.position_manager.functions.subscriber(token_id).call() + ) + return_value = subscriber + return return_value + + def position_manager_get_is_support_interface( + self, + interface_id: bytes, + ) -> bool: + """ + :param interface_id: The interface ID to check; should be 'bytes4' + :returns: True if specifeid interface is supported by the PositionManager contract + """ + if len(interface_id) != 4: + raise ValueError("interface_id should be 4 bytes long") + is_supported: bool = bool( + self.position_manager.functions.supportsInterface(interface_id).call() + ) + return_value = is_supported + return return_value + + def position_manager_get_symbol( + self, + ) -> str: + """ + :returns: The symbol of the PositionManager token + """ + symbol: str = str(self.position_manager.functions.symbol().call()) + return_value = symbol + return return_value + + def position_manager_get_token_descriptor( + self, + ) -> str: + """ + :returns: The address of the PositionDescriptor contract as a string + """ + token_descriptor: str = str( + self.position_manager.functions.tokenDescriptor().call() + ) + return_value = token_descriptor + return return_value + + def position_manager_get_position_uri(self, token_id: int) -> str: + """ + :returns: The URI of the position manager's ERC721-compliant metadata for a given token ID + """ + uri: str = str(self.position_manager.functions.tokenURI(token_id).call()) + return_value = uri + return return_value + + def position_manager_get_unsubscribe_gas_limit(self) -> int: + """ + :returns: The gas limit used when unsubscribing from a position. + """ + unsubscribe_gas_limit: int = ( + self.position_manager.functions.unsubscribeGasLimit().call() + ) + return_value = unsubscribe_gas_limit + return return_value + + # Write methods + def position_manager_approve(self, spender: str, token_id: int) -> HexBytes: + """ + Change or reaffirm the approved address for an NFT + Zero address removes existing approval. + """ + function = self.position_manager.functions.approve(spender, token_id) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def position_manager_initialize_pool( + self, pool_key: PoolKey, sqrt_price_x96: int, payable_amount: int + ) -> HexBytes: + """ + Initialize a Uniswap v4 Pool with the given parameters. + """ + function = self.position_manager.functions.initializePool( + astuple(pool_key), sqrt_price_x96 + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_modify_liquidities( + self, unlock_data: bytes, deadline: int, payable_amount: int + ) -> HexBytes: + """ + Unlocks Uniswap v4 PoolManager and batches actions for modifying liquidity + """ + function = self.position_manager.functions.modifyLiquidities( + unlock_data, deadline + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_modify_liquidities_without_unlock( + self, actions: bytes, params: list[bytes], payable_amount: int + ) -> HexBytes: + """ + Batches actions for modifying liquidity without unlocking v4 PoolManager + + This must be called by a contract that has already unlocked the v4 PoolManager + """ + function = self.position_manager.functions.modifyLiquiditiesWithoutUnlock( + actions, params + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_multicall( + self, data: list[bytes], payable_amount: int + ) -> HexBytes: + """ + Call multiple functions in the current contract in a single transaction, with the possibility of sending ETH along with the calls. + """ + function = self.position_manager.functions.multicall(data) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_permit( + self, + spender: str, + token_id: int, + deadline: int, + nonce: int, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Approve of a specific token ID for spending by spender via signature + """ + function = self.position_manager.functions.permit( + spender, token_id, deadline, nonce, signature + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_permit2_single( + self, + owner: str, + permit_single: PermitSingle, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Allows forwarding a single permit to permit2 + """ + function = self.position_manager.functions.permit( + owner, astuple(permit_single), signature + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_permit2_batch( + self, + owner: str, + permit_batch: PermitBatch, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Allows forwarding batch permits to permit2 + """ + function = self.position_manager.functions.permit( + owner, astuple(permit_batch), signature + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_permit_for_all( + self, + owner: str, + operator: str, + approved: bool, + deadline: int, + nonce: int, + signature: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Set an operator with full permission to an owner's tokens via signature + """ + function = self.position_manager.functions.permitForAll( + owner, operator, approved, deadline, nonce, signature + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_revoke_nonce( + self, nonce: int, payable_amount: int + ) -> HexBytes: + """ + Revoke a nonce by spending it, preventing it from being used again + """ + function = self.position_manager.functions.revokeNonce(nonce) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_safe_transfer_from( + self, from_addr: str, to_addr: str, token_id: int, payable_amount: int + ) -> HexBytes: + """ + Transfer a position from one address to another + """ + function = self.position_manager.functions.safeTransferFrom( + from_addr, to_addr, token_id + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_safe_transfer_from_with_data( + self, + from_addr: str, + to_addr: str, + token_id: int, + data: bytes, + payable_amount: int, + ) -> HexBytes: + """ + Transfer a position from one address to another with additional data + """ + function = self.position_manager.functions.safeTransferFrom( + from_addr, to_addr, token_id, data + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_set_approval_for_all( + self, operator: str, approved: bool, payable_amount: int + ) -> HexBytes: + """ + Enable or disable approval for a third party ("operator") to manage all of `msg.sender`'s assets + """ + function = self.position_manager.functions.setApprovalForAll(operator, approved) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_subscribe( + self, token_id: int, new_subscriber: str, data: bytes, payable_amount: int + ) -> HexBytes: + """ + Enables the subscriber to receive notifications for a respective position + """ + function = self.position_manager.functions.subscribe( + token_id, new_subscriber, data + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_transfer_from( + self, from_addr: str, to_addr: str, token_id: int, payable_amount: int + ) -> HexBytes: + """ + Overrides solmate transferFrom in case a notification to subscribers is needed + """ + function = self.position_manager.functions.transferFrom( + from_addr, to_addr, token_id + ) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def position_manager_unsubscribe( + self, token_id: int, payable_amount: int + ) -> HexBytes: + """ + Removes the subscriber from receiving notifications for a respective position + """ + function = self.position_manager.functions.unsubscribe(token_id) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + # PoolManager methods + # Read methods + def pool_manager_get_allowance( + self, owner: str, spender: str, token_id: int + ) -> int: + """ + Spender allowance of an id. + """ + allowance: int = int( + self.pool_manager.functions.allowance(owner, spender, token_id).call() + ) + return_value = allowance + return return_value + + def pool_manager_get_balance_of(self, address: str, token_id: int) -> int: + """ + The number of tokens in owner's address. + """ + balance: int = int( + self.pool_manager.functions.balanceOf(address, token_id).call() + ) + return_value = balance + return return_value + + def pool_manager_get_extsload(self, slot: bytes) -> bytes: + """ + Called by external contracts to access granular pool state + """ + value: bytes = self.pool_manager.functions.extsload(slot).call() + return_value = value + return return_value + + def pool_manager_get_extsload_sequence( + self, start_slot: bytes, slots_count: int + ) -> list[bytes]: + """ + Called by external contracts to access a sequence of storage slots + """ + value: list[bytes] = self.pool_manager.functions.extsload( + start_slot, slots_count + ).call() + return_value = value + return return_value + + def pool_manager_get_extsload_sparse(self, slots: list[bytes]) -> list[bytes]: + """ + Called by external contracts to access a sparse set of storage slots + """ + value: list[bytes] = self.pool_manager.functions.extsload(slots).call() + return_value = value + return return_value + + def pool_manager_get_exttload_sparse(self, slots: list[bytes]) -> list[bytes]: + """ + Called by external contracts to access sparse transient pool state + """ + value: list[bytes] = self.pool_manager.functions.exttload(slots).call() + return_value = value + return return_value + + def pool_manager_get_exttload(self, slot: bytes) -> bytes: + """ + Called by external contracts to access transient storage of the contract + """ + value: bytes = self.pool_manager.functions.exttload(slot).call() + return_value = value + return return_value + + def pool_manager_get_is_operator(self, owner: str, operator: str) -> bool: + """ + Checks if a spender is approved by an owner as an operator + """ + is_operator: bool = self.pool_manager.functions.isOperator( + owner, operator + ).call() + return_value = is_operator + return return_value + + def pool_manager_get_owner(self) -> str: + """ + Retrieve the contract owner. + """ + owner: str = str(self.pool_manager.functions.owner().call()) + return_value = owner + return return_value + + def pool_manager_get_protocol_fee_controller(self) -> str: + """ + Returns the current protocol fee controller address + """ + protocol_fee_controller: str = str( + self.pool_manager.functions.protocolFeeController().call() + ) + return_value = protocol_fee_controller + return return_value + + def pool_manager_get_protocol_fees_accrued(self, address: str) -> int: + """ + Given a currency address, returns the protocol fees accrued in that currency. + """ + protocol_fees_accrued: int = int( + self.pool_manager.functions.protocolFeesAccrued(address).call() + ) + return_value = protocol_fees_accrued + return return_value + + def pool_manager_get_supports_interface(self, interface_id: bytes) -> bool: + """ + Checks if a given interface ID is supported by the contract + + :param interface_id: The interface ID to check; should be `bytes4` + :returns: True if specifeid interface is supported by the PoolManager contract + """ + if len(interface_id) != 4: + raise ValueError("interface_id should be 4 bytes long") + supports_interface: bool = bool( + self.pool_manager.functions.supportsInterface(interface_id).call() + ) + return_value = supports_interface + return return_value + + # Write methods + def pool_manager_approve( + self, spender: str, token_id: int, amount: int + ) -> HexBytes: + """ + Approves an amount of an id to a spender. + """ + function = self.pool_manager.functions.approve(spender, token_id, amount) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_burn(self, from_addr: str, token_id: int, amount: int) -> HexBytes: + """ + Called by the user to move value from ERC6909 balance. + """ + function = self.pool_manager.functions.burn(from_addr, token_id, amount) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_clear(self, currency: str, amount: int) -> HexBytes: + """ + !!!WARNING!!! - Any currency that is cleared, will be non-retrievable, and locked in the contract permanently. + A call to clear will zero out a positive balance WITHOUT a corresponding transfer. + This could be used to clear a balance that is considered dust. + Additionally, the amount must be the exact positive balance. + This is to enforce that the caller is aware of the amount being cleared. + """ + function = self.pool_manager.functions.clear(currency, amount) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_collect_protocol_fees( + self, recipient: str, currency: str, amount: int + ) -> HexBytes: + """ + Collects the protocol fees for a given recipient and currency, returning the amount collected + This will revert if the contract is unlocked + """ + function = self.pool_manager.functions.collectProtocolFees( + recipient, currency, amount + ) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_donate( + self, pool_key: PoolKey, amount0: int, amount1: int, hook_data: bytes + ) -> HexBytes: + """ + Donate the given currency amounts to the in-range liquidity providers of a pool + """ + function = self.pool_manager.functions.donate( + astuple(pool_key), amount0, amount1, hook_data + ) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_initialize( + self, pool_key: PoolKey, sqrt_price_x96: int + ) -> HexBytes: + """ + Initialize the state for a given pool ID. + """ + function = self.pool_manager.functions.initialize( + astuple(pool_key), sqrt_price_x96 + ) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_mint(self, to_addr: str, token_id: int, amount: int) -> HexBytes: + """ + Called by the user to move value into ERC6909 balance. + """ + function = self.pool_manager.functions.mint(to_addr, token_id, amount) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_modify_liquidity( + self, + pool_key: PoolKey, + liquidity_params: ModifyLiquidityParams, + hook_data: bytes, + ) -> HexBytes: + """ + Modify the liquidity for the given pool. + """ + function = self.pool_manager.functions.modifyLiquidity( + astuple(pool_key), astuple(liquidity_params), hook_data + ) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_set_operator(self, operator: str, approved: bool) -> HexBytes: + """ + Sets or removes an operator for the caller. + """ + function = self.pool_manager.functions.setOperator(operator, approved) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_set_protocol_fee( + self, pool_key: PoolKey, new_protocol_fee: int + ) -> HexBytes: + """ + Sets the protocol fee for the given pool. + """ + function = self.pool_manager.functions.setProtocolFee( + astuple(pool_key), new_protocol_fee + ) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_set_protocol_fee_controller(self, controller: str) -> HexBytes: + """ + Sets a new protocol fee controller. + """ + function = self.pool_manager.functions.setProtocolFeeController(controller) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_settle(self, payable_amount: int) -> HexBytes: + """ + Called by the user to pay what is owed. + """ + function = self.pool_manager.functions.settle() + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def pool_manager_settle_for(self, recipient: str, payable_amount: int) -> HexBytes: + """ + Called by the user to pay on behalf of another address. + """ + function = self.pool_manager.functions.settleFor(recipient) + tx = self._build_and_send_tx( + function, self._get_tx_params(value=payable_amount) + ) + return tx + + def pool_manager_swap( + self, pool_key: PoolKey, params: SwapParams, hook_data: bytes + ) -> HexBytes: + """ + Swap against the given pool. + """ + function = self.pool_manager.functions.swap( + astuple(pool_key), astuple(params), hook_data + ) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_sync(self, currency: str) -> HexBytes: + """ + Writes the current ERC20 balance of the specified currency to transient storage. + This is used to checkpoint balances for the manager and derive deltas for the caller. + This MUST be called before any ERC20 tokens are sent into the contract, see documentation for more details. + """ + function = self.pool_manager.functions.sync(currency) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_take(self, currency: str, to_addr: str, amount: int) -> HexBytes: + """ + Called by the user to net out some value owed to the user. + """ + function = self.pool_manager.functions.take(currency, to_addr, amount) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_transfer( + self, to_addr: str, token_id: int, amount: int + ) -> HexBytes: + """ + Transfers an amount of an id from the caller to a receiver. + """ + function = self.pool_manager.functions.transfer(to_addr, token_id, amount) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_transfer_from( + self, sender: str, receiver: str, token_id: int, amount: int + ) -> HexBytes: + """ + Transfers an amount of an id from a sender to a receiver. + """ + function = self.pool_manager.functions.transferFrom( + sender, receiver, token_id, amount + ) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_transfer_ownership(self, new_owner: str) -> HexBytes: + """ + Transfers ownership of the contract to a new owner. + """ + function = self.pool_manager.functions.transferOwnership(new_owner) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_unlock(self, data: bytes) -> HexBytes: + """ + All interactions on the contract that account deltas require unlocking. + A caller that calls `unlock` must implement `IUnlockCallback(msg.sender).unlockCallback(data)`, + where they interact with the remaining functions on this contract. + """ + function = self.pool_manager.functions.unlock(data) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + def pool_manager_update_dynamic_lp_fee( + self, pool_key: PoolKey, new_dynamic_lp_fee: int + ) -> HexBytes: + """ + Updates the pools lp fees for the a pool that has enabled dynamic lp fees. + """ + function = self.pool_manager.functions.updateDynamicLPFee( + astuple(pool_key), new_dynamic_lp_fee + ) + tx = self._build_and_send_tx(function, self._get_tx_params()) + return tx + + # Tokens price functions + def get_token_token_spot_price( + self, + token0: str, + token1: str, + fee: int = 500, + tick_spacing: int = 10, + hooks: str = ZERO_HOOK, + ) -> float: + """ + :param token0: The first token in the pair. + :param token1: The second token in the pair. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :return: The current spot price for token to token trades. + """ + + if token0.lower() < token1.lower(): + den0 = self.get_token(_str_to_addr(token0)).decimals + den1 = self.get_token(_str_to_addr(token1)).decimals + zero_for_one = True + else: + den0 = self.get_token(_str_to_addr(token1)).decimals + den1 = self.get_token(_str_to_addr(token0)).decimals + zero_for_one = False + + if token0.lower() > token1.lower(): + token1, token0 = token0, token1 + + spot_price_x96: int = self.stateview_get_slot0( + token0, token1, fee, tick_spacing, hooks + )["sqrtPriceX96"] + + spot_price: float = (spot_price_x96 * spot_price_x96 * 10**den0 >> (96 * 2)) / ( + 10**den1 + ) + if not zero_for_one: + spot_price = 1 / spot_price + return spot_price + + # Estimates slippage for the given amount of token0 + def estimate_price_impact( + self, + token0: str, + token1: str, + qty: int, + fee: int = 500, + tick_spacing: int = 10, + hooks: str = ZERO_HOOK, + hook_data: bytes = b"", + ) -> float: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token0` to be sold. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :return: the estimated price impact as a positive float (0.01 = 1%). + + See ``examples/v4_examples.py`` for an example which uses this. + + NOTE: Does not support multi-hop swaps, should be used for single-hop swaps only. + """ + + try: + spot_price = self.get_token_token_spot_price( + token0, token1, fee, tick_spacing, hooks + ) + except (ArithmeticError, BadFunctionCallOutput): + # ArithmeticError is raised when `token0` amount in the pool + # equals 0. + # BadFunctionCallOutput is raised when the pool for + # given `(token0, token1, fee)` doesn't exist + return 1 + + if spot_price == 0: + # Occurs when `token1` amount in the pool equals 0 + return 1 + try: + quote_amount = self.get_quote_exact_input_single( + token0, token1, qty, fee, tick_spacing, hooks, hook_data + ) + except ContractLogicError: + # ContractLogicError is raised when the pool's contract for given + # `(token0, token1, fee)` hasn't been deployed. + return 1 + price = ( + quote_amount / (qty / (10 ** self.get_token(_str_to_addr(token0)).decimals)) + ) / 10 ** self.get_token(_str_to_addr(token1)).decimals + + # calculate and subtract the realised fees from the price impact. See: + # https://github.com/uniswap-python/uniswap-python/issues/310 + price_impact_with_fees: float = (spot_price - price) / spot_price + fee_realised_percentage: float = realised_fee_percentage(fee, qty) + price_impact_real: float = price_impact_with_fees - fee_realised_percentage + return price_impact_real + + # Quoter methods + # Read methods + def get_quote_exact_input_single( + self, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str = ZERO_HOOK, + hook_data: bytes = b"", + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token0` to be sold. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :return: Quote for token to token single hop trades with an exact input. + """ + if token0.lower() < token1.lower(): + zero_for_one = True + else: + zero_for_one = False + token0, token1 = token1, token0 + pool_key = (token0, token1, fee, tick_spacing, hooks) + # [0]=The output quote [1]=estimated gas units used for the swap + quote_amount: int = self.quoter.functions.quoteExactInputSingle( + (pool_key, zero_for_one, qty, hook_data) + ).call()[0] + return quote_amount + + def get_quote_exact_input( + self, + token_exact: str, + qty: int, + route: list[PoolKey], + ) -> int: + """ + :param token_exact: The token for which the qty parameter is specified. + :param qty: The amount of the token_exact to be swapped. + :param route: The list of PoolKeys representing the path of the swap, starting with the pool containing the input token and ending with the pool containing the output token. + :return: Quote for token to token multi-hop trades with an exact input. + """ + encoded_route = self.encode_path_keys_input(route, token_exact) + + # [0]=The output quote [1]=estimated gas units used for the swap + quote_amount: int = self.quoter.functions.quoteExactInput( + ( + token_exact, + [astuple(path_key) for path_key in encoded_route], + qty, + ) + ).call()[0] + return quote_amount + + def get_quote_exact_output_single( + self, + token0: str, + token1: str, + qty: int, + fee: int, + tick_spacing: int, + hooks: str = ZERO_HOOK, + hook_data: bytes = b"", + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token1` to be bought. + :param fee: The fee tier for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :return: Quote for token to token single hop trades with an exact output. + """ + if token0.lower() < token1.lower(): + zero_for_one = True + else: + zero_for_one = False + token1, token0 = token0, token1 + + pool_key = ( + token0, + token1, + fee, + tick_spacing, + hooks, + ) + # [0]=The input quote [1]=estimated gas units used for the swap + quote_amount: int = self.quoter.functions.quoteExactOutputSingle( + (pool_key, zero_for_one, qty, hook_data) + ).call()[0] + return quote_amount + + def get_quote_exact_output( + self, + token_exact: str, + qty: int, + route: list[PoolKey], + ) -> int: + """ + :param token_exact: The token for which the qty parameter is specified, either the input or output token depending on the quote type. + :param qty: The amount of the token_exact to be swapped. For an exact output quote, this is the amount of output token desired. + :param route: The list of PoolKeys representing the path of the swap in reverse order, starting with the pool containing the output token and ending with the pool containing the input token. + :return: Quote for token to token multi-hop trades with an exact output. + """ + + encoded_route = self.encode_path_keys_output(route, token_exact) + quote_amount: int = self.quoter.functions.quoteExactOutput( + ( + token_exact, + [astuple(path_key) for path_key in encoded_route], + qty, + ) + ).call()[0] + return quote_amount + + # Market price functions for selling `qty` amount of `token0` to buy `token1` + def get_price_input( + self, + token0: str, + token1: str, + qty: int, + fee: int | None = None, + tick_spacing: int | None = None, + hooks: str | None = ZERO_HOOK, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token0` to be sold. + :param fee: Optional. The fee tier for the swap. + + :param tick_spacing: Optional. The tick spacing for the swap. + :param hooks: Optional. The hooks for the swap. + :param hook_data: Optional. The hook data for the swap. + :param route: Optional. The route for the swap. The first element should be the pool containing `token0` and the last element should be the pool containing `token1`. + :return: Returns the exact amount of `token1` that can be received for `qty` amount of `token0` at the current price. + + If `route` is provided, it will be used for the quote. Otherwise, `fee` and `tick_spacing` must be provided for a single hop quote. + """ + result: int = 0 + if route is None: + if fee is None or tick_spacing is None: + raise ValueError( + "fee and tick_spacing parameters must be provided for single hop quotes" + ) + result = self.get_quote_exact_input_single( + token0, + token1, + qty, + fee, + tick_spacing, + hooks, # type: ignore[arg-type] + hook_data, # type: ignore[arg-type] + ) + else: + result = self.get_quote_exact_input(token0, qty, route) + return result + + def get_price_output( + self, + token0: str, + token1: str, + qty: int, + fee: int | None = None, + tick_spacing: int | None = None, + hooks: str | None = ZERO_HOOK, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + ) -> int: + """ + :param token0: The token to be sold. + :param token1: The token to be bought. + :param qty: The amount of `token1` to be bought. + :param fee: Optional. The fee tier for the swap. + :param tick_spacing: Optional. The tick spacing for the swap. + :param hooks: Optional. The hooks for the swap. + :param hook_data: Optional. The hook data for the swap. + :param route: Optional. The route for the swap in reverse order. The first element should be the pool containing `token1` and the last element should be the pool containing `token0`. + :return: Returns the exact amount of `token0` required to buy `qty` amount of `token1` at the current price. + + If `route` is provided, it will be used for the quote. Otherwise, `fee` and `tick_spacing` must be provided for a single hop quote. + """ + result: int = 0 + if route is None: + if fee is None or tick_spacing is None or hooks is None: + raise ValueError( + "fee, tick_spacing, and hooks parameters must be provided for single hop quotes" + ) + result = self.get_quote_exact_output_single( + token0, + token1, + qty, + fee, + tick_spacing, + hooks, + hook_data, # type: ignore[arg-type] + ) + else: + result = self.get_quote_exact_output(token1, qty, route) + return result + + # Swap functions + def token_to_token_swap_exact_input( + self, + input_token: str, + qty: int, + qtycap: int, + output_token: str, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes = b"", + min_hop_price_x_36: int = 0, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param qty: The amount of `input_token` to be sold. + :param qtycap: The minimum amount of `output_token` that must be bought for the swap to succeed. + :param output_token: The token to be bought. + :param fee: The fee for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :param min_hop_price_x_36: Optional. The minimum price for each hop in the route, expressed as an integer. + :param custom_nonce: Optional. The nonce to use for the transaction. + + Swaps an exact amount of `input_token` for a minimum amount of `output_token`, + reverting if the amount of `output_token` received is less than `qtycap`. + """ + min_tokens_bought: int = int((1 - self.max_slippage) * qtycap) + + ether_amount: int = 0 + if input_token == ETH_ADDRESS: + ether_amount = qty + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8"], + [ + v4_actions["SWAP_EXACT_IN_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ) + + # SETTING PARAMS + if input_token.lower() < output_token.lower(): + zero_for_one = True + token0, token1 = input_token, output_token + else: + zero_for_one = False + token0, token1 = output_token, input_token + exact_input_single_params: bytes = encode( + v4_actions_abis["SWAP_EXACT_IN_SINGLE"], + [ + ( + (token0, token1, fee, tick_spacing, hooks), + zero_for_one, + qty, + min_tokens_bought, + min_hop_price_x_36, + hook_data, + ) + ], + ) + settle_all_params: bytes = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, qty], + ) + take_all_params: bytes = encode( + v4_actions_abis["TAKE_ALL"], + [output_token, min_tokens_bought], + ) + + # ENCODING DATA + params = [exact_input_single_params, settle_all_params, take_all_params] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + def token_to_token_swap_input( + self, + input_token: str, + qty: int, + qtycap: int, + route: list[PoolKey], + min_hop_price_x_36: list[int] = [], + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param qty: The amount of `input_token` to be sold. + :param qtycap: The minimum amount of `output_token` that must be bought for the swap to succeed. + :param route: The list of PoolKeys representing the path of the swap. The first element should be the pool containing the `input_token` and the last element should be the pool containing the `output_token`. + :param min_hop_price_x_36: Optional. A list of minimum prices for each hop in the route, expressed as a list of integers. + :param custom_nonce: Optional. The nonce to use for the transaction. + + Swaps an exact amount of `input_token` for a minimum amount of `output_token` through a specified multi-hop route, + reverting if the amount of `output_token` received is less than `qtycap`. + """ + min_tokens_bought: int = int((1 - self.max_slippage) * qtycap) + + encoded_route = self.encode_path_keys_input(route, input_token) + + ether_amount: int = 0 + + if input_token == ETH_ADDRESS: + ether_amount = qty + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_IN, SETTLE_ALL, TAKE_ALL + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8"], + [ + v4_actions["SWAP_EXACT_IN"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + ], + ) + + # SETTING PARAMS + exact_input_params: bytes = encode( + v4_actions_abis["SWAP_EXACT_IN"], + [ + ( + input_token, + [astuple(path_key) for path_key in encoded_route], + min_hop_price_x_36, + qty, + min_tokens_bought, + ) + ], + ) + settle_all_params: bytes = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, qty], + ) + take_all_params: bytes = encode( + v4_actions_abis["TAKE_ALL"], + [ + _addr_to_str(encoded_route[-1].intermediate_currency), # type: ignore[arg-type] + min_tokens_bought, + ], + ) + + # ENCODING DATA + params = [exact_input_params, settle_all_params, take_all_params] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + def token_to_token_swap_exact_output( + self, + input_token: str, + qty: int, + qtycap: int, + output_token: str, + fee: int, + tick_spacing: int, + hooks: str, + hook_data: bytes = b"", + min_hop_price_x_36: int = 0, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param qty: The amount of `output_token` to be bought. + :param qtycap: The maximum amount of `input_token` that can be sold for the trade to proceed. + :param output_token: The token to be bought. + :param fee: The fee for the swap. + :param tick_spacing: The tick spacing for the swap. + :param hooks: The hooks for the swap. + :param hook_data: The hook data for the swap. + :param min_hop_price_x_36: Optional. The minimum price for each hop in the route, expressed as an integer. + :param custom_nonce: Optional. The nonce for the transaction. + """ + + amount_in_max: int = int((1 + self.max_slippage) * qtycap) + + ether_amount: int = 0 + if input_token == ETH_ADDRESS: + ether_amount = amount_in_max + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_OUT_SINGLE, SETTLE_ALL, TAKE_ALL, TAKE + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + args=[ + v4_actions["SWAP_EXACT_OUT_SINGLE"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + v4_actions["TAKE"], + ], + ) + # SETTING PARAMS + if input_token.lower() < output_token.lower(): + zero_for_one = True + token0, token1 = input_token, output_token + else: + zero_for_one = False + token0, token1 = output_token, input_token + exact_output_single_params = encode( + v4_actions_abis["SWAP_EXACT_OUT_SINGLE"], + [ + ( + ( + token0, + token1, + fee, + tick_spacing, + hooks, + ), + zero_for_one, + qty, + amount_in_max, + min_hop_price_x_36, + hook_data, + ) + ], + ) + settle_all_params = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, amount_in_max], + ) + take_all_params = encode( + v4_actions_abis["TAKE_ALL"], + [output_token, qty], + ) + take_input_params = encode( + v4_actions_abis["TAKE"], [input_token, _addr_to_str(self.address), 0] + ) + + # ENCODING DATA + params = [ + exact_output_single_params, + settle_all_params, + take_all_params, + take_input_params, + ] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + def token_to_token_swap_output( + self, + output_token: str, + qty: int, + qtycap: int, + route: list[PoolKey], + min_hop_price_x_36: list[int] = [], + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param output_token: The token to be bought. + :param qty: The amount of `output_token` to be bought. + :param qtycap: The maximum amount of `input_token` that can be sold for the trade to proceed. + :param route: The route for the swap in reverse order. The first element should be the pool containing `output_token` and the last element should be the pool containing `input_token`. + :param min_hop_price_x_36: Optional. The minimum price for each hop in the route, expressed as a list of integers. + :param custom_nonce: Optional. The nonce for the transaction. + + Swaps a maximum amount of `input_token` for an exact amount of `output_token` through a specified multi-hop route, + reverting if the amount of `input_token` required is more than `qtycap`. + """ + + amount_in_max: int = int((1 + self.max_slippage) * qtycap) + encoded_route = self.encode_path_keys_output(route, output_token) + + input_token: str = _addr_to_str(encoded_route[0].intermediate_currency) # type: ignore[arg-type] + + ether_amount: int = 0 + if input_token == ETH_ADDRESS: + ether_amount = amount_in_max + + # V4_SWAP // Encode swap commands and actions + commands: bytes = encode_packed( + ["uint8"], + args=[universal_router_commands["V4_SWAP"]], + ) + + # Actions are SWAP_EXACT_OUT, SETTLE_ALL, TAKE_ALL, TAKE + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + args=[ + v4_actions["SWAP_EXACT_OUT"], + v4_actions["SETTLE_ALL"], + v4_actions["TAKE_ALL"], + v4_actions["TAKE"], + ], + ) + # SETTING PARAMS + exact_output_params: bytes = encode( + v4_actions_abis["SWAP_EXACT_OUT"], + [ + ( + output_token, + [astuple(path_key) for path_key in encoded_route], + min_hop_price_x_36, + qty, + amount_in_max, + ) + ], + ) + settle_all_params: bytes = encode( + v4_actions_abis["SETTLE_ALL"], + [input_token, amount_in_max], + ) + take_all_params: bytes = encode( + v4_actions_abis["TAKE_ALL"], + [output_token, qty], + ) + take_input_params = encode( + v4_actions_abis["TAKE"], [input_token, _addr_to_str(self.address), 0] + ) + + # ENCODING DATA + params = [ + exact_output_params, + settle_all_params, + take_all_params, + take_input_params, + ] + inputs = [] + inputs.append( + encode( + ["bytes", "bytes[]"], + [actions, params], + ) + ) + + return self._build_and_send_tx( + self.router.functions.execute(commands, inputs, self._deadline()), + self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + def drop_txn( + self, + address_to: AddressLike, + gas_price: float, + priority_fee: int = 10, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Replaces pending transaction with zero-value ETH transfer + + :param address_to: Zero address or any other valid address to which the zero-value transaction will be sent + + Params `gas_price` and `priority_fee` are Gas Price and Max Priority Fee respectively; + MUST be at least 20% higher than values the original transaction has. + """ + # This one is for legacy transactions + transaction_dict_legacy = { + "nonce": self.w3.eth.get_transaction_count(self.address) + if custom_nonce is None + else custom_nonce, + "from": _addr_to_str(self.address), + "to": Web3.to_checksum_address(address_to), + "value": Web3.to_wei(0, "wei"), + "gasPrice": Web3.to_wei(gas_price, "gwei"), + "gas": int(self.gas_limit), + "chainId": int(self.w3.eth.chain_id), + } + signed_txn = self.w3.eth.account.sign_transaction( + transaction_dict_legacy, + self.private_key, + ) + # This one is for post-Merge transactions + transaction_dict = { + "type": 2, + "nonce": self.w3.eth.get_transaction_count(self.address) + if custom_nonce is None + else custom_nonce, + "from": _addr_to_str(self.address), + "to": _addr_to_str(address_to), + "value": Web3.to_wei(0, "wei"), + "maxFeePerGas": Web3.to_wei(int(gas_price), "gwei"), + "maxPriorityFeePerGas": Web3.to_wei(priority_fee, "gwei"), + "gas": int(self.gas_limit), + "chainId": int(self.w3.eth.chain_id), + } + signed_txn_london = self.w3.eth.account.sign_transaction( + transaction_dict, + self.private_key, + ) + if self.post_merge: + return self.w3.eth.send_raw_transaction(signed_txn_london.raw_transaction) + else: + return self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) + + # Market functions for swapping `qty` amount of `token0` to buy `token1` + def make_swap_input( + self, + input_token: str, + output_token: str, + qty: int, + qtycap: int, + swap_pool_key: PoolKey | None = None, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param output_token: The token to be bought. + :param qty: The amount of `input_token` to be sold. + :param qtycap: The minimum amount of `output_token` that must be bought for the trade to proceed. + :param swap_pool_key: Optional. The PoolKey for the swap, required if `route` is not provided. + :param hook_data: Optional. The hook data for the swap. This is only used for single hop swaps and will be ignored if `route` is provided. + :param route: Optional. The route for the swap. The first element should be the pool containing `input_token` and the last element should be the pool containing `output_token`. If `route` is provided, it will be used for the swap instead of `swap_pool_key`. + :param custom_nonce: Optional. The nonce for the transaction. + + Make a trade by defining the qty of the input token. + If `route` is provided, it will be used for the swap. Otherwise, `swap_pool_key` must be provided for a single hop swap.""" + result: HexBytes | None = None + if route is None: + if swap_pool_key is None: + raise ValueError("swap_pool_key must be provided for single hop swaps") + result = self.token_to_token_swap_exact_input( + input_token, + qty, + qtycap, + output_token, + swap_pool_key.fee, + swap_pool_key.tick_spacing, + swap_pool_key.hooks, + hook_data, # type: ignore[arg-type] + min_hop_price_x_36=0, + custom_nonce=custom_nonce, + ) + else: + result = self.token_to_token_swap_input( + input_token, + qty, + qtycap, + route, + min_hop_price_x_36=[], + custom_nonce=custom_nonce, + ) + return result + + def make_swap_output( + self, + input_token: str, + output_token: str, + qty: int, + qtycap: int, + swap_pool_key: PoolKey | None = None, + hook_data: bytes | None = b"", + route: list[PoolKey] | None = None, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + :param input_token: The token to be sold. + :param output_token: The token to be bought. + :param qty: The amount of `output_token` to be bought. + :param qtycap: The maximum amount of `input_token` that can be sold for the trade to proceed. + :param swap_pool_key: Optional. The PoolKey for the swap, required if `route` is not provided. + :param hook_data: Optional. The hook data for the swap. + :param route: Optional. The route for the swap in reverse order. The first element should be the pool containing `output_token` and the last element should be the pool containing `input_token`. + + Make a trade by defining the qty of the output token. + If `route` is provided, it will be used for the swap. Otherwise, `swap_pool_key` must be provided for a single hop swap. + """ + result: HexBytes | None = None + if route is None: + if swap_pool_key is None: + raise ValueError("swap_pool_key must be provided for single hop swaps") + result = self.token_to_token_swap_exact_output( + input_token, + qty, + qtycap, + output_token, + swap_pool_key.fee, + swap_pool_key.tick_spacing, + swap_pool_key.hooks, + hook_data, # type: ignore[arg-type] + min_hop_price_x_36=0, + custom_nonce=custom_nonce, + ) + else: + result = self.token_to_token_swap_output( + output_token, + qty, + qtycap, + route, + min_hop_price_x_36=[], + custom_nonce=custom_nonce, + ) + return result + + # Liquidity management functions + def get_position_info(self, token_id: int) -> dict: + """ + Get information about a liquidity position given its token ID. + :return: A dictionary with the following keys: + + - currency0: The address of the first token in the pool + - currency1: The address of the second token in the pool + - fee: The fee tier of the pool + - tickSpacing: The tick spacing of the pool + - hooks: The hooks address of the pool + - poolID: The truncated pool ID of the position, which is the first 25 bytes of the full pool ID + - tickLower: The lower tick of the position + - tickUpper: The upper tick of the position + - hasSubscriber: A boolean indicating whether the position has a subscriber + - owner: The address of the owner of the position + """ + position_info = self.position_manager_get_pool_and_position_info(token_id) + + pool_key: PoolKey = position_info["poolKey"] + pool_info = position_info["info"] + pool_info_decoded = self.decode_position_info(pool_info) + owner_of = self.position_manager_get_owner_of(token_id) + return_value: dict = { + "currency0": pool_key.currency0, + "currency1": pool_key.currency1, + "fee": pool_key.fee, + "tickSpacing": pool_key.tick_spacing, + "hooks": pool_key.hooks, + "poolID": pool_info_decoded["poolID"], + "tickLower": pool_info_decoded["tickLower"], + "tickUpper": pool_info_decoded["tickUpper"], + "hasSubscriber": pool_info_decoded["hasSubscriber"], + "owner": owner_of, + } + return return_value + + def get_position_value( + self, token_id: int, token0_decimals: int, token1_decimals: int + ) -> dict: + """ + Get the value of a liquidity position given its token ID. + """ + liquidity: int = self.position_manager_get_position_liquidity(token_id) + position_info = self.get_position_info(token_id) + slot0 = self.stateview_get_slot0( + position_info["currency0"], + position_info["currency1"], + position_info["fee"], + position_info["tickSpacing"], + position_info["hooks"], + ) + # price: Decimal = Decimal( + # Decimal( + # decode_sqrt_ratioX96( + # slot0["sqrtPriceX96"], + # ) + # ) + # / Decimal(10 ** (token1_decimals - token0_decimals)) + # ) + sqrt_price = int(slot0["sqrtPriceX96"]) + raw_price: Decimal = (sqrt_price / Decimal(Q96)) ** 2 + decimal_factor: Decimal = 10**token1_decimals / Decimal(10**token0_decimals) + price: Decimal = raw_price / decimal_factor + amounts = self.get_amounts_for_liquidity_by_ticks( + slot0["sqrtPriceX96"], + position_info["tickLower"], + position_info["tickUpper"], + liquidity, + ) + amount0: Decimal = Decimal(amounts["amount0"]) / Decimal(10**token0_decimals) + amount1: Decimal = Decimal(amounts["amount1"]) / Decimal(10**token1_decimals) + return_value: dict = { + "amount0": amounts["amount0"], + "amount1": amounts["amount1"], + # NOTE: unclaimed fees are not yet computed; total values equal principal only + "unclaimed_fees0": 0, + "unclaimed_fees1": 0, + "total_amount0": amounts["amount0"], + "total_amount1": amounts["amount1"], + "value_in_token0": amount0 + amount1 / price, + "value_in_token1": amount1 + amount0 * price, + } + return return_value + + def create_pool( + self, + pool_key: PoolKey, + sqrt_price_x96: int, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Creates a new liquidity pool without initial liquidity with the specified parameters and a starting price. + """ + function = self.position_manager.functions.initializePool( + astuple(pool_key), + sqrt_price_x96, + ) + tx = self._build_and_send_tx( + function, + self._get_tx_params(custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + def mint_position( + self, + pool_key: PoolKey, + tick_lower: int, + tick_upper: int, + liquidity: int, + amount0: int, + amount1: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Mints a new liquidity position with the specified parameters. + + :param pool_key: The parameters of the pool for which the position is being minted. + :param tick_lower: The lower tick of the position. + :param tick_upper: The upper tick of the position. + :param liquidity: The amount of liquidity to mint. + :param amount0: The amount of token0 to provide as liquidity. + :param amount1: The amount of token1 to provide as liquidity. + :param recipient: The address that will receive excessive ETH amounts. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the minting. + :return: The transaction hash of the minting transaction. + """ + + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: MINT_POSITION, SETTLE_PAIR, SWEEP, SWEEP + if pool_key.currency0 == ETH_ADDRESS: + ether_amount = amount0 + actions: bytes = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + [ + v4_actions["MINT_POSITION"], + v4_actions["SETTLE_PAIR"], + v4_actions["SWEEP"], + v4_actions["SWEEP"], + ], + ) + + # Encoding params + mint_position_params: bytes = encode( + [ + "(address,address,uint24,int24,address)", + "int24", + "int24", + "uint256", + "uint128", + "uint128", + "address", + "bytes", + ], + [ + astuple(pool_key), + tick_lower, + tick_upper, + liquidity, + amount0, + amount1, + recipient, + hook_data, + ], + ) + settle_pair_params: bytes = encode( + ["address", "address"], + [pool_key.currency0, pool_key.currency1], + ) + sweep0_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency0, recipient], + ) + sweep1_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency1, recipient], + ) + params: list[bytes] = [ + mint_position_params, + settle_pair_params, + sweep0_params, + sweep1_params, + ] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + def increase_liquidity( + self, + pool_key: PoolKey, + token_id: int, + amount0_max: int, + amount1_max: int, + liquidity: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Increases the liquidity of an existing position. + + :param pool_key: The parameters of the pool for which the position is being increased. + :param token_id: The token ID of the position to increase liquidity for. + :param amount0_max: The maximum amount of token0 to provide as liquidity. + :param amount1_max: The maximum amount of token1 to provide as liquidity. + :param liquidity: The amount of liquidity to add to the position. + :param recipient: The address that will receive excessive ETH amounts. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the liquidity increase. + :return: The transaction hash of the liquidity increase transaction. + """ + + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: INCREASE_LIQUIDITY, SETTLE_PAIR, SWEEP, SWEEP + if pool_key.currency0 == ETH_ADDRESS: + ether_amount = amount0_max + + actions = encode_packed( + ["uint8", "uint8", "uint8", "uint8"], + [ + v4_actions["INCREASE_LIQUIDITY"], + v4_actions["SETTLE_PAIR"], + v4_actions["SWEEP"], + v4_actions["SWEEP"], + ], + ) + # Encoding params + increase_liquidity_params: bytes = encode( + ["uint256", "uint256", "uint128", "uint128", "bytes"], + [ + token_id, + liquidity, + amount0_max, + amount1_max, + hook_data, + ], + ) + settle_pair_params: bytes = encode( + ["address", "address"], + [pool_key.currency0, pool_key.currency1], + ) + sweep0_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency0, recipient], + ) + sweep1_params: bytes = encode( + v4_actions_abis["SWEEP"], + [pool_key.currency1, recipient], + ) + params: list[bytes] = [ + increase_liquidity_params, + settle_pair_params, + sweep0_params, + sweep1_params, + ] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + def decrease_liquidity( + self, + pool_key: PoolKey, + token_id: int, + amount0_min: int, + amount1_min: int, + liquidity: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Decreases the liquidity of an existing position. + + :param pool_key: The parameters of the pool for which the position is being decreased. + :param token_id: The token ID of the position to decrease liquidity for. + :param amount0_min: The minimum amount of token0 to receive from the liquidity decrease. + :param amount1_min: The minimum amount of token1 to receive from the liquidity decrease. + :param liquidity: The amount of liquidity to remove from the position. + :param recipient: The address that will receive the withdrawn liquidity. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the liquidity decrease. + :return: The transaction hash of the liquidity decrease transaction. + """ + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: DECREASE_LIQUIDITY, TAKE_PAIR + actions = encode_packed( + ["uint8", "uint8"], + [ + v4_actions["DECREASE_LIQUIDITY"], + v4_actions["TAKE_PAIR"], + ], + ) + # Encoding params + decrease_liquidity_params: bytes = encode( + ["uint256", "uint256", "uint128", "uint128", "bytes"], + [ + token_id, + liquidity, + amount0_min, + amount1_min, + hook_data, + ], + ) + take_pair_params: bytes = encode( + ["address", "address", "address"], + [pool_key.currency0, pool_key.currency1, recipient], + ) + params: list[bytes] = [decrease_liquidity_params, take_pair_params] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + def collect_fees( + self, + pool_key: PoolKey, + token_id: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Collects the fees accrued by an existing position. + + :param pool_key: The parameters of the pool for which the position is collecting fees. + :param token_id: The token ID of the position for which to collect fees. + :param recipient: The address that will receive the collected fees. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the fee collection. + :return: The transaction hash of the fee collection transaction. + """ + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: DECREASE_LIQUIDITY, TAKE_PAIR + actions = encode_packed( + ["uint8", "uint8"], + [ + v4_actions["DECREASE_LIQUIDITY"], + v4_actions["TAKE_PAIR"], + ], + ) + # Encoding params + decrease_liquidity_params: bytes = encode( + ["uint256", "uint256", "uint128", "uint128", "bytes"], + [ + token_id, + 0, + 0, + 0, + hook_data, + ], + ) + take_pair_params: bytes = encode( + ["address", "address", "address"], + [pool_key.currency0, pool_key.currency1, recipient], + ) + params: list[bytes] = [decrease_liquidity_params, take_pair_params] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + def burn_position( + self, + pool_key: PoolKey, + token_id: int, + amount0_min: int, + amount1_min: int, + recipient: str | None = None, + hook_data: bytes | None = b"", + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Burns an existing liquidity position. + + :param pool_key: The parameters of the pool for which the position is being burned. + :param token_id: The token ID of the position to burn. + :param amount0_min: The minimum amount of token0 to receive from burning the position. + :param amount1_min: The minimum amount of token1 to receive from burning the position. + :param recipient: The address that will receive the withdrawn liquidity. If None, it defaults to the caller's address. + :param hook_data: Optional bytes that can be passed to the hooks during the position burn. + :return: The transaction hash of the position burn transaction. + """ + ether_amount: int = 0 + if recipient is None: + recipient = _addr_to_str(self.address) + # Encoding actions: BURN_POSITION, TAKE_PAIR + actions = encode_packed( + ["uint8", "uint8"], + [ + v4_actions["BURN_POSITION"], + v4_actions["TAKE_PAIR"], + ], + ) + # Encoding params + burn_position_params: bytes = encode( + ["uint256", "uint128", "uint128", "bytes"], + [ + token_id, + amount0_min, + amount1_min, + hook_data, + ], + ) + take_pair_params: bytes = encode( + ["address", "address", "address"], + [pool_key.currency0, pool_key.currency1, recipient], + ) + params: list[bytes] = [burn_position_params, take_pair_params] + + # Encoding unlock data + unlock_data: bytes = encode( + ["bytes", "bytes[]"], + [actions, params], + ) + + tx: HexBytes = self._build_and_send_tx( + self.position_manager.functions.modifyLiquidities( + unlock_data, self._deadline() + ), + self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + return tx + + def universal_router_execute( + self, + commands: list[int], + actions: list[list[int]], + params: list[list[list]], + ether_amount: int = 0, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """ + Executes a transaction with the Universal Router with the specified commands, actions, and parameters. + + :param commands: A list of command integers corresponding to the commands to execute. + :param actions: A list of lists of action integers, where each sublist corresponds to the actions for the respective command in the `commands` list. + :param params: A list of lists of lists of parameters, where each sublist corresponds to the parameters for the respective actions in the `actions` list. The structure of the parameters should match the expected input for each action as defined in the Universal Router documentation. + :param ether_amount: The amount of ether to send with the transaction, if any. + :param custom_nonce: Optional. The nonce for the transaction. + + See ``tests/test_uniswap4.py`` for an example which uses this, `test_universal_router_execute_multiaction()` specific test. + + NOTE: This function does not perform checks on the validity of the commands ``SWAP_EXACT_IN*`` and ``SWAP_EXACT_OUT*``. It is the caller's responsibility to ensure that they are correct and will not cause the transaction to revert. + """ + # Validating input parameters + ignore_list = [ + "SWAP_EXACT_IN_SINGLE", + "SWAP_EXACT_IN", + "SWAP_EXACT_OUT_SINGLE", + "SWAP_EXACT_OUT", + ] + if len(commands) != len(actions) or len(actions) != len(params): + raise ValueError("Lists' lengths are not equal.") + for commands_item, actions_item, params_item in zip(commands, actions, params): + command_key: str = self._get_dict_key_by_value( + universal_router_commands, commands_item + ) + if len(actions_item) == 0: + if len(universal_router_commands_abis[command_key]) != sum( + len(sub_list) for sub_list in params_item + ): + raise ValueError("ABI mismatch for " + command_key + " command.") + else: + for specific_action, specific_param in zip(actions_item, params_item): + action_key: str = self._get_dict_key_by_value( + v4_actions, specific_action + ) + if action_key in ignore_list: + continue + if len(v4_actions_abis[action_key]) != len(specific_param): + raise ValueError( + "ABI mismatch for " + + action_key + + " command in " + + command_key + + " command." + ) + + # Encoding data + commands_abi = ["uint8"] * len(commands) + encoded_commands: bytes = encode_packed(commands_abi, commands) + encoded_inputs: list[bytes] = [] + for commands_item, actions_item, params_item in zip(commands, actions, params): + command_key = self._get_dict_key_by_value( + universal_router_commands, commands_item + ) + if len(actions_item) == 0: + encoded_params: bytes = encode( + universal_router_commands_abis[command_key], params_item[0] + ) + encoded_inputs.append(encoded_params) + else: + encoded_actions_with_params = self.encode_actions_with_params( + actions_item, params_item + ) + encoded_params = encode( + universal_router_commands_abis[command_key], + [ + encoded_actions_with_params["actions"], + encoded_actions_with_params["params"], + ], + ) + encoded_inputs.append(encoded_params) + # Execute contract call + result: HexBytes = self._build_and_send_tx( + self.router.functions.execute( + encoded_commands, encoded_inputs, self._deadline() + ), + self._get_tx_params(value=ether_amount, custom_nonce=custom_nonce), + custom_nonce=custom_nonce, + ) + + return result + + # Helper functions + def encode_actions_with_params( + self, actions: list[int], params: list[list] + ) -> dict: + encoded_actions_abi = ["uint8"] * len(actions) + encoded_actions: bytes = encode_packed(encoded_actions_abi, actions) + encoded_params: list[bytes] = [] + for actions_item, params_item in zip(actions, params): + action_key = self._get_dict_key_by_value(v4_actions, actions_item) + encoded_params_item: bytes = encode( + v4_actions_abis[action_key], params_item + ) + encoded_params.append(encoded_params_item) + return_value: dict = { + "actions": encoded_actions, + "params": encoded_params, + } + return return_value + + @staticmethod + def _get_dict_key_by_value(param_dict: dict, value: int) -> str: + return_value = next((k for k, v in param_dict.items() if v == value), None) + if return_value is None: + raise IndexError("Key is not found.") + return str(return_value) + + @staticmethod + def get_liquidity_for_amount0( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, amount0: int + ) -> int: + """ + Helper function to calculate the amount of liquidity that can be provided for a given amount of `token0` and price range defined by `sqrt_ ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + liquidity: int = (amount0 * (sqrt_ratio_a_x96 * sqrt_ratio_b_x96 // Q96)) // ( + sqrt_ratio_b_x96 - sqrt_ratio_a_x96 + ) + return liquidity + + @staticmethod + def get_liquidity_for_amount1( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, amount1: int + ) -> int: + """ + Helper function to calculate the amount of liquidity that can be provided for a given amount of `token1` and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + liquidity: int = (amount1 * Q96) // (sqrt_ratio_b_x96 - sqrt_ratio_a_x96) + return liquidity + + def get_liquidity_for_amounts( + self, + sqrt_ratio_a_x96: int, + sqrt_ratio_b_x96: int, + sqrt_ratio_current_x96: int, + amount0: int, + amount1: int, + ) -> int: + """ + Helper function to calculate the amount of liquidity that can be provided for given amounts of `token0` and `token1` and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + if sqrt_ratio_current_x96 <= sqrt_ratio_a_x96: + liquidity: int = self.get_liquidity_for_amount0( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, amount0 + ) + elif sqrt_ratio_current_x96 < sqrt_ratio_b_x96: + liquidity0: int = self.get_liquidity_for_amount0( + sqrt_ratio_current_x96, sqrt_ratio_b_x96, amount0 + ) + liquidity1: int = self.get_liquidity_for_amount1( + sqrt_ratio_a_x96, sqrt_ratio_current_x96, amount1 + ) + liquidity = min(liquidity0, liquidity1) + else: + liquidity = self.get_liquidity_for_amount1( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, amount1 + ) + return liquidity + + @staticmethod + def get_amount0_for_liquidity( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, liquidity: int + ) -> int: + """ + Helper function to calculate the amount of `token0` that can be provided for a given amount of liquidity and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + amount0: int = ( + (liquidity * Q96 * (sqrt_ratio_b_x96 - sqrt_ratio_a_x96)) + // sqrt_ratio_b_x96 + // sqrt_ratio_a_x96 + ) + return amount0 + + @staticmethod + def get_amount1_for_liquidity( + sqrt_ratio_a_x96: int, sqrt_ratio_b_x96: int, liquidity: int + ) -> int: + """ + Helper function to calculate the amount of `token1` that can be provided for a given amount of liquidity and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + amount1: int = (liquidity * (sqrt_ratio_b_x96 - sqrt_ratio_a_x96)) // Q96 + return amount1 + + def get_amounts_for_liquidity( + self, + sqrt_ratio_a_x96: int, + sqrt_ratio_b_x96: int, + sqrt_ratio_current_x96: int, + liquidity: int, + ) -> dict: + """ + Helper function to calculate the amounts of `token0` and `token1` that can be provided for a given amount of liquidity and price range defined by `sqrt_ratio_a_x96` and `sqrt_ratio_b_x96`. + """ + if sqrt_ratio_a_x96 > sqrt_ratio_b_x96: + sqrt_ratio_a_x96, sqrt_ratio_b_x96 = sqrt_ratio_b_x96, sqrt_ratio_a_x96 + + amount0: int = 0 + amount1: int = 0 + if sqrt_ratio_current_x96 <= sqrt_ratio_a_x96: + amount0 = self.get_amount0_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, liquidity + ) + elif sqrt_ratio_current_x96 < sqrt_ratio_b_x96: + amount0 = self.get_amount0_for_liquidity( + sqrt_ratio_current_x96, sqrt_ratio_b_x96, liquidity + ) + amount1 = self.get_amount1_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_current_x96, liquidity + ) + else: + amount1 = self.get_amount1_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, liquidity + ) + return_value: dict = { + "amount0": amount0, + "amount1": amount1, + } + return return_value + + def get_amounts_for_liquidity_by_ticks( + self, ratio_current_x96: int, tick_lower: int, tick_upper: int, liquidity: int + ) -> dict: + sqrt_ratio_a_x96 = get_sqrt_ratio_at_tick(tick_lower) + sqrt_ratio_b_x96 = get_sqrt_ratio_at_tick(tick_upper) + return_value: dict = self.get_amounts_for_liquidity( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, ratio_current_x96, liquidity + ) + return return_value + + def get_liquidity_for_amounts_by_ticks( + self, + ratio_current_x96: int, + tick_lower: int, + tick_upper: int, + amount0: int, + amount1: int, + ) -> int: + sqrt_ratio_a_x96 = get_sqrt_ratio_at_tick(tick_lower) + sqrt_ratio_b_x96 = get_sqrt_ratio_at_tick(tick_upper) + liquidity = self.get_liquidity_for_amounts( + sqrt_ratio_a_x96, sqrt_ratio_b_x96, ratio_current_x96, amount0, amount1 + ) + return liquidity + + def get_minted_token_id(self, tx_hash: str) -> list[int]: + """ + Helper function to extract the token ID of a newly minted position from the transaction receipt of the minting transaction. + + :return: A list of token IDs of the newly minted positions; empty list if none can be extracted. In most cases, this list will contain only one token ID, but in some cases (e.g., if multiple positions are minted in a single transaction), it may contain multiple token IDs. + """ + transaction_receipt = self.w3.eth.get_transaction_receipt(tx_hash) # type: ignore [arg-type] + logs = self.position_manager.events.Transfer().process_receipt( + transaction_receipt + ) + return_value: list[int] = [] + for log in logs: + try: + if _addr_to_str(log.args["from"]) == ZERO_HOOK: + minted_token_id: int = log.args.id + return_value.append(minted_token_id) + except (AttributeError, KeyError): + logger.warning( + "Could not extract minted token ID from transaction receipt for transaction hash: %s.", + tx_hash, + ) + return return_value + + @staticmethod + def decode_position_info(position_info: int) -> dict: + """ + + :return: + A dictionary with the following keys: + - `tickLower`: The lower tick of the position. + - `tickUpper`: The upper tick of the position. + - `poolID`: The truncated pool ID of the position, which is the first 25 bytes of the full pool ID. + - `hasSubscriber`: A boolean indicating whether the position has a subscriber. + """ + tick_lower_offset = 8 + tick_upper_offset = 32 + pool_id_offset = 56 + sign_bit = 0x800000 + + # The hasSubscriber flag is stored in the least significant byte of the position info, so we can use a bitwise AND operation with a mask of 0xFF to extract it. + mask = 0xFF + has_subscriber: bool = (position_info & mask) != 0 + + # The tick lower and tick upper values are stored in the next 3 bytes each, so we shift the position info to the right by the respective offsets and use a bitwise AND operation with a mask of 0xFFFFFF to extract them. We also check if the sign bit is set to determine if the tick values are negative, and if so, we subtract 0x1000000 from them to get the correct negative value. + mask = 0xFFFFFF + tick_lower: int = (position_info >> tick_lower_offset) & mask + if tick_lower & sign_bit: + tick_lower = tick_lower - 0x1000000 + + tick_upper: int = (position_info >> tick_upper_offset) & mask + if tick_upper & sign_bit: + tick_upper = tick_upper - 0x1000000 + + # The pool ID is stored in the remaining bytes, so we shift the position info to the right by the pool ID offset to get the pool ID. + rest_part: int = position_info >> pool_id_offset + pool_id: bytes = rest_part.to_bytes(25, byteorder="big") + + return_value: dict = { + "tickLower": tick_lower, + "tickUpper": tick_upper, + "poolID": pool_id, + "hasSubscriber": has_subscriber, + } + return return_value + + @staticmethod + def encode_path_keys_input( + path: list[PoolKey], + currency_in: str, + hook_data_list: list[bytes] | None = None, + ) -> list[PathKey]: + """ + Encodes a list of PoolKeys into the format expected by the quoter for multi-hop ExactInput quotes. + """ + encoded_path: list[PathKey] = [] + if hook_data_list is None: + hook_data_list = [b""] * len(path) + else: + if len(hook_data_list) != len(path): + raise ValueError("Length of hook_data_list must match length of path") + for pool_key, hook_data in zip(path, hook_data_list): + currency_out: str = ( + pool_key.currency1 + if currency_in.lower() == pool_key.currency0.lower() + else pool_key.currency0 + ) + path_key: PathKey = PathKey( + currency_out, + pool_key.fee, + pool_key.tick_spacing, + pool_key.hooks, + hook_data, + ) + encoded_path.append(path_key) + currency_in = currency_out + return encoded_path + + @staticmethod + def encode_path_keys_output( + path: list[PoolKey], + currency_out: str, + hook_data_list: list[bytes] | None = None, + ) -> list[PathKey]: + """ + Encodes a list of PoolKeys into the format expected by the quoter for multi-hop ExactOutput quotes. + + """ + encoded_path: list[PathKey] = [] + if hook_data_list is None: + hook_data_list = [b""] * len(path) + else: + if len(hook_data_list) != len(path): + raise ValueError("Length of hook_data_list must match length of path") + for pool_key, hook_data in zip(reversed(path), reversed(hook_data_list)): + currency_in: str = ( + pool_key.currency1 + if currency_out.lower() == pool_key.currency0.lower() + else pool_key.currency0 + ) + path_key: PathKey = PathKey( + currency_in, + pool_key.fee, + pool_key.tick_spacing, + pool_key.hooks, + hook_data, + ) + encoded_path.insert(0, path_key) + currency_out = currency_in + return encoded_path + + @staticmethod + def get_pool_id(pool: PoolKey) -> HexBytes: + """Computes the pool ID for a given PoolKey by hashing its parameters.""" + pool_data = eth_abi.abi.encode( + types=["address", "address", "uint24", "int24", "address"], + args=[ + pool.currency0, + pool.currency1, + pool.fee, + pool.tick_spacing, + pool.hooks, + ], + ) + pool_id = Web3.keccak(pool_data) + return pool_id + + def get_token(self, address: AddressLike, abi_name: str = "erc20") -> ERC20Token: + """ + Retrieves metadata from the ERC20 contract of a given token, like its name, symbol, and decimals. + """ + if address == ETH_ADDRESS or address == _str_to_addr(ETH_ADDRESS): + return ERC20Token( + address=address, + name="ETH", + symbol="ETH", + decimals=18, + ) + token_contract = _load_contract(self.w3, abi_name, address=address) + try: + _name = token_contract.functions.name().call() + _symbol = token_contract.functions.symbol().call() + decimals = token_contract.functions.decimals().call() + except ValueError: + raise InvalidToken(address) + try: + name = _name.decode() + except (ValueError, AttributeError): + name = str(_name) + try: + symbol = _symbol.decode() + except (ValueError, AttributeError) as e: + logger.warning( + "Error occurred while decoding symbol for %s: %s", + _addr_to_str(address), + e, + ) + symbol = str(_symbol) + return ERC20Token(symbol, address, name, decimals) + + def get_token_balance(self, erc20: AddressLike) -> Decimal: + """Get the balance of an ERC20 token for your address.""" + contract = _load_contract(self.w3, abi_name="erc20", address=erc20) + decimals: int = contract.functions.decimals().call() + balance: int = contract.functions.balanceOf(self.address).call() + return_balance: Decimal = Decimal(balance) / Decimal(10**decimals) + return return_balance + + def get_balance(self) -> Decimal: + """Get the balance of ETH for your address.""" + balance: int = self.w3.eth.get_balance(self.address) + return_balance: Decimal = Decimal(balance) / Decimal(10**18) + return return_balance + + def load_contract_with_abi(self, abi_name: str, address: AddressLike) -> Contract: + return self.w3.eth.contract(address=address, abi=_load_abi(abi_name)) + + def erc20_contract(self, token_addr: AddressLike) -> Contract: + return self.load_contract_with_abi(abi_name="erc20", address=token_addr) + + def _deadline(self) -> int: + """Get a predefined deadline. 10min by default.""" + return int(time.time()) + 10 * 60 + + def _get_tx_params( + self, value: int = 0, custom_nonce: Nonce | None = None + ) -> TxParams: + """Get generic transaction parameters.""" + if custom_nonce is not None and custom_nonce < Nonce(0): + raise ValueError("Nonce can only be a positive integer.") + if not self.post_merge: + return { + "from": _addr_to_str(self.address), + "value": Wei(value), + "gas": int(self.gas_limit), + "gasPrice": Web3.to_wei(self.gas_price, "gwei"), + "nonce": Nonce(max(self.last_nonce, 0)) + if custom_nonce is None + else custom_nonce, + } + else: + return { + "from": _addr_to_str(self.address), + "gas": int(self.gas_limit), + "maxPriorityFeePerGas": Web3.to_wei(self.priority_fee, "gwei"), + "maxFeePerGas": Web3.to_wei(self.gas_price, "gwei"), + "type": 2, + "chainId": self.w3.eth.chain_id, + "value": Wei(value), + "nonce": Nonce(max(self.last_nonce, 0)) + if custom_nonce is None + else custom_nonce, + } + + def _build_and_send_tx( + self, + function: ContractFunction, + tx_params: TxParams | None = None, + custom_nonce: Nonce | None = None, + ) -> HexBytes: + """Build and send a transaction.""" + if not tx_params: + tx_params = self._get_tx_params(custom_nonce=custom_nonce) + transaction = function.build_transaction(tx_params) + signed_txn = self.w3.eth.account.sign_transaction( + transaction, private_key=self.private_key + ) + try: + return self.w3.eth.send_raw_transaction(signed_txn.raw_transaction) + finally: + # logger.debug(f"nonce: {tx_params['nonce']}") + if custom_nonce is None: + self.last_nonce = Nonce(tx_params["nonce"] + 1) diff --git a/uniswap/util.py b/uniswap/util.py new file mode 100644 index 00000000..cf7db0a9 --- /dev/null +++ b/uniswap/util.py @@ -0,0 +1,692 @@ +import asyncio +import json +import math +import os +from collections.abc import Generator, Sequence +from time import sleep +from typing import ( + Any, +) +from xml.etree import ElementTree as ET + +from web3 import AsyncWeb3, Web3 +from web3.contract import AsyncContract, Contract +from web3.exceptions import NameNotFound + +from .constants import ( + MAX_TICK, + MIN_TICK, + _netid_to_name, + _poolmanager_contract_addresses_v4, + _tick_spacing, +) +from .types import Address, AddressLike, PoolKey + + +def _str_to_addr(s: AddressLike | str) -> Address: + """Idempotent""" + if isinstance(s, str): + if s.startswith("0x"): + return Address(bytes.fromhex(s[2:])) + else: + raise NameNotFound(f"Couldn't convert string '{s}' to AddressLike") + else: + return s + + +def _addr_to_str(a: AddressLike) -> str: + if isinstance(a, bytes): + # Address or ChecksumAddress + addr: str = Web3.to_checksum_address("0x" + bytes(a).hex()) + return addr + elif isinstance(a, str) and a.startswith("0x"): + addr = Web3.to_checksum_address(a) + return addr + + raise NameNotFound(a) + + +def is_same_address(a1: AddressLike | str, a2: AddressLike | str) -> bool: + return bool(_str_to_addr(a1) == _str_to_addr(a2)) + + +def _validate_address(a: AddressLike) -> None: + assert _addr_to_str(a) + + +def _load_abi(name: str) -> str: + path = f"{os.path.dirname(os.path.abspath(__file__))}/assets/" + with open(os.path.abspath(path + f"{name}.abi")) as f: + abi: str = json.load(f) + return abi + + +def _load_contract(w3: Web3, abi_name: str, address: AddressLike) -> Contract: + address = Web3.to_checksum_address(address) + return w3.eth.contract(address=address, abi=_load_abi(abi_name)) + + +async def _load_contract_async( + w3: AsyncWeb3, abi_name: str, address: AddressLike +) -> AsyncContract: + address = Web3.to_checksum_address(address) + return w3.eth.contract(address=address, abi=_load_abi(abi_name)) + + +def _load_contract_erc20(w3: Web3, address: AddressLike) -> Contract: + return _load_contract(w3, "erc20", address) + + +def _encode_path(token_in: AddressLike, route: list[tuple[int, AddressLike]]) -> bytes: + """ + Needed for multi-hop swaps in V3. + + https://github.com/Uniswap/uniswap-v3-sdk/blob/1a74d5f0a31040fec4aeb1f83bba01d7c03f4870/src/utils/encodeRouteToPath.ts + """ + raise NotImplementedError + + +# Adapted from: https://github.com/Uniswap/v3-sdk/blob/main/src/utils/encodeSqrtRatioX96.ts +def encode_sqrt_ratioX96(amount_0: int, amount_1: int) -> int: + numerator = amount_1 << 192 + denominator = amount_0 + ratioX192 = numerator // denominator + 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 + + +def get_sqrt_ratio_at_tick(tick: int) -> int: + """ + Helper function to calculate the square root price ratio at a given tick. + """ + + # NOTE See https://github.com/Uniswap/sdks/blob/main/sdks/v3-sdk/src/utils/tickMath.ts + + if tick < MIN_TICK or tick > MAX_TICK: + raise ValueError("Tick out of bounds.") + + abs_tick: int = abs(tick) + ratio: int = ( + 0xFFFCB933BD6FAD37AA2D162D1A594001 + if (abs_tick & 0x1) != 0 + else 0x100000000000000000000000000000000 + ) + if (abs_tick & 0x2) != 0: + ratio = _mul_shift(ratio, 0xFFF97272373D413259A46990580E213A) + if (abs_tick & 0x4) != 0: + ratio = _mul_shift(ratio, 0xFFF2E50F5F656932EF12357CF3C7FDCC) + if (abs_tick & 0x8) != 0: + ratio = _mul_shift(ratio, 0xFFE5CACA7E10E4E61C3624EAA0941CD0) + if (abs_tick & 0x10) != 0: + ratio = _mul_shift(ratio, 0xFFCB9843D60F6159C9DB58835C926644) + if (abs_tick & 0x20) != 0: + ratio = _mul_shift(ratio, 0xFF973B41FA98C081472E6896DFB254C0) + if (abs_tick & 0x40) != 0: + ratio = _mul_shift(ratio, 0xFF2EA16466C96A3843EC78B326B52861) + if (abs_tick & 0x80) != 0: + ratio = _mul_shift(ratio, 0xFE5DEE046A99A2A811C461F1969C3053) + if (abs_tick & 0x100) != 0: + ratio = _mul_shift(ratio, 0xFCBE86C7900A88AEDCFFC83B479AA3A4) + if (abs_tick & 0x200) != 0: + ratio = _mul_shift(ratio, 0xF987A7253AC413176F2B074CF7815E54) + if (abs_tick & 0x400) != 0: + ratio = _mul_shift(ratio, 0xF3392B0822B70005940C7A398E4B70F3) + if (abs_tick & 0x800) != 0: + ratio = _mul_shift(ratio, 0xE7159475A2C29B7443B29C7FA6E889D9) + if (abs_tick & 0x1000) != 0: + ratio = _mul_shift(ratio, 0xD097F3BDFD2022B8845AD8F792AA5825) + if (abs_tick & 0x2000) != 0: + ratio = _mul_shift(ratio, 0xA9F746462D870FDF8A65DC1F90E061E5) + if (abs_tick & 0x4000) != 0: + ratio = _mul_shift(ratio, 0x70D869A156D2A1B890BB3DF62BAF32F7) + if (abs_tick & 0x8000) != 0: + ratio = _mul_shift(ratio, 0x31BE135F97D08FD981231505542FCFA6) + if (abs_tick & 0x10000) != 0: + ratio = _mul_shift(ratio, 0x9AA508B5B7A84E1C677DE54F3E99BC9) + if (abs_tick & 0x20000) != 0: + ratio = _mul_shift(ratio, 0x5D6AF8DEDB81196699C329225EE604) + if (abs_tick & 0x40000) != 0: + ratio = _mul_shift(ratio, 0x2216E584F5FA1EA926041BEDFE98) + if (abs_tick & 0x80000) != 0: + ratio = _mul_shift(ratio, 0x48A170391F7DC42444E8FA2) + + if tick > 0: + ratio = ( + 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF // ratio + ) + + return_value = (ratio // (1 << 32)) + (0 if (ratio % (1 << 32)) == 0 else 1) + + return return_value + + +def _mul_shift(x: int, y: int) -> int: + """ + Helper function to perform multiplication followed by a right shift. + """ + product: int = x * y + shifted: int = product >> 128 + return shifted + + +# 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] + return -(MIN_TICK // -min_tick_spacing) * min_tick_spacing + + +def get_max_tick(fee: int) -> int: + max_tick_spacing: int = _tick_spacing[fee] + return (MAX_TICK // max_tick_spacing) * max_tick_spacing + + +def default_tick_range(fee: int) -> tuple[int, int]: + min_tick = get_min_tick(fee) + max_tick = get_max_tick(fee) + + return min_tick, max_tick + + +def nearest_tick(tick: int, fee: int) -> int: + min_tick, max_tick = default_tick_range(fee) + assert min_tick <= tick <= max_tick, ( + f"Provided tick is out of bounds: {(min_tick, max_tick)}" + ) + + tick_spacing = _tick_spacing[fee] + rounded_tick_spacing = round(tick / tick_spacing) * tick_spacing + + if rounded_tick_spacing < min_tick: + return rounded_tick_spacing + tick_spacing + elif rounded_tick_spacing > max_tick: + return rounded_tick_spacing - tick_spacing + else: + return rounded_tick_spacing + + +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 + + +class V4pools: + """Uniswap V4 pools handler""" + + poolkeys_list: list[PoolKey] + + def __init__( + self, + web3: Web3, + ): + """:param web3: Web3 instance connected to the network for which pool data is being fetched.""" + self.poolkeys_list: list[PoolKey] = [] + self.web3 = web3 + self.last_block = 0 + + def get_last_block( + self, + ) -> int: + """Returns last block number processed by fetch_poolkey_data() method.""" + return self.last_block + + def set_last_block(self, value: int) -> None: + """Sets last block number processed by fetch_poolkey_data() method.""" + self.last_block = value + + def fetch_poolkey_data( + self, + first_block: int, + chunk_size: int = 5, + clear_list: bool = True, + retry_attempts: int = 3, + minutes_between_retries: int = 3, + last_block: int | None = None, + ) -> int: + """ + :param first_block: Starting block for scanning process + :param chunk_size: Defines amount of blocks per single log request + :param clear_list: When True, clears pool list before log scanning, when False - new entries will be added to the end of the list. + :param retry_attempts: Number of attempts to retry and resume log retrieval in case of RPC returns errors like `500` etc. + :param minutes_between_retries: Minutes to wait between retry attempts. + :param last_block: Optional parameter defining the last block for scanning process. If None, current block number will be used. + :return: 0 if logs were successfully processed, -1 if logs retrieval failed (e.g. due to wrong chunk size or RPC endpoint failure). + """ + # Scans PoolManager contract' Initialize() event logs in order to get + # list of all pools. See documentation for suggested starting blocks. + # chunk_size default value 5 should be suitable for public RPCs; + # can be increased on paid or local RPCs for better performance. + + chain_id = int(self.web3.net.version) + net_name = _netid_to_name[chain_id] + pool_manager_contract_address = _poolmanager_contract_addresses_v4[net_name] + pool_manager_contract = _load_contract( + self.web3, + "uniswap-v4/poolmanager", + _str_to_addr(pool_manager_contract_address), + ) + first_block_number: int = first_block + if last_block is None: + last_block_number: int = int(self.web3.eth.get_block_number()) + else: + last_block_number = min( + max(first_block, last_block), int(self.web3.eth.get_block_number()) + ) + + chunks_amount = int((last_block_number - first_block_number) // chunk_size) + start_block = first_block_number + end_block = 0 + + print( + f"Logs processing started, start block = {start_block}; end block = {last_block_number}." + ) + if clear_list: + self.poolkeys_list.clear() + retry_attempts_done: int = 0 + for i in range(chunks_amount + 1): + if start_block + chunk_size <= last_block_number: + end_block = start_block + chunk_size + else: + end_block = last_block_number + print( + f"Processing chunk {i}/{chunks_amount}; (start block = {start_block}; end block = {end_block})", + end="\r", + flush=True, + ) + try: + logs = pool_manager_contract.events.Initialize().get_logs( + from_block=start_block, to_block=end_block + ) + except Exception as e: + # Exception occurs when chunk size value is too big so RPC endpoint rejects + # requests OR RPC endpoint has issues. + # In such cases, we will try to resume log retrieval process for a defined number of attempts. If all attempts fail, the method will be aborted and `-1`` value will be returned. + print(f"Error retrieving logs: {e}") + while retry_attempts_done < retry_attempts: + print() + print() + print( + f"Error retrieving logs. Retrying. ({retry_attempts_done + 1}/{retry_attempts})" + ) + print( + f"Waiting for {minutes_between_retries} minutes before next attempt..." + ) + sleep(int(minutes_between_retries) * 60) + retry_attempts_done += 1 + try: + logs = pool_manager_contract.events.Initialize().get_logs( + from_block=start_block, to_block=end_block + ) + print("Issue addressed. Resuming log retrieval.") + retry_attempts_done = 0 + break + except Exception as e_reconnect: + print(f"Attempt {retry_attempts_done} failed: {e_reconnect}") + if retry_attempts_done == retry_attempts: + print( + "Couldn't retrieve logs; check chunk size and RPC availability. Aborted. " + ) + print(f"Error details: {e}") + return -1 + for log_item in logs: + try: + pool_currency0 = str(log_item.args.currency0) + pool_currency1 = str(log_item.args.currency1) + pool_fee = int(str(log_item.args.fee)) + pool_tick_spacing = int(str(log_item.args.tickSpacing)) + pool_hooks = str(log_item.args.hooks) + pool: PoolKey = PoolKey( + pool_currency0, + pool_currency1, + pool_fee, + pool_tick_spacing, + pool_hooks, + ) + if pool not in self.poolkeys_list: + self.poolkeys_list.append(pool) + except AttributeError as e: + print(f"Error occurred while processing log item: {e}") + continue + self.set_last_block(end_block) + if end_block == last_block_number: + break + start_block = start_block + chunk_size + + print( + "---------------------------------------------------------------------------------------------" + ) + print(f"Logs processing completed. Last block processed {last_block_number}") + self.set_last_block(last_block_number) + return 0 + + def save_poolkeys_list(self, poolkey_data_filename: str) -> None: + """Saves poolKey list to specified file (XML format)""" + pool_data = ET.Element("PoolData") + + for pool_item in self.poolkeys_list: + pool = ET.SubElement(pool_data, "Pool") + + currency0 = ET.SubElement(pool, "Currency0") + currency0.text = str(pool_item.currency0) + + currency1 = ET.SubElement(pool, "Currency1") + currency1.text = str(pool_item.currency1) + + fee = ET.SubElement(pool, "Fee") + fee.text = str(pool_item.fee) + + tick_spacing = ET.SubElement(pool, "TickSpacing") + tick_spacing.text = str(pool_item.tick_spacing) + + hooks = ET.SubElement(pool, "Hooks") + hooks.text = str(pool_item.hooks) + + ET.ElementTree(pool_data).write(poolkey_data_filename) + + def load_poolkeys_list(self, poolkey_data_filename: str) -> None: + """Loads poolKey list from specified file (XML format)""" + if os.path.isfile(poolkey_data_filename): + try: + tree = ET.parse(poolkey_data_filename) + except ET.ParseError: + raise ValueError( + "Parse error, file seems to be corrupted (" + + poolkey_data_filename + + ")" + ) + self.poolkeys_list.clear() + root = tree.getroot() + for item in root: + pool_currency0 = str(item[0].text) + pool_currency1 = str(item[1].text) + pool_fee = int(str(item[2].text)) + pool_tick_spacing = int(str(item[3].text)) + pool_hooks = str(item[4].text) + pool: PoolKey = PoolKey( + pool_currency0, + pool_currency1, + pool_fee, + pool_tick_spacing, + pool_hooks, + ) + self.poolkeys_list.append(pool) + else: + raise ValueError("Couldn't locate file " + poolkey_data_filename) + + def get_poolkeys_sublist(self, currency0: str, currency1: str) -> list[PoolKey]: + """Returns all pools for the (currency0, currency1) pair""" + if currency0.lower() < currency1.lower(): + c0, c1 = currency0.lower(), currency1.lower() + else: + c0, c1 = currency1.lower(), currency0.lower() + result_list = [ + x + for x in self.poolkeys_list + if c0 == x.currency0.lower() and c1 == x.currency1.lower() + ] + return result_list + + +class AsyncV4pools: + """Uniswap V4 pools handler""" + + poolkeys_list: list[PoolKey] + + def __init__( + self, + web3: AsyncWeb3, + ): + """:param web3: Web3 instance connected to the network for which pool data is being fetched.""" + self.poolkeys_list: list[PoolKey] = [] + self.web3 = web3 + self.last_block = 0 + + def get_last_block( + self, + ) -> int: + """Returns last block number processed by fetch_poolkey_data() method.""" + return self.last_block + + def set_last_block(self, value: int) -> None: + """Sets last block number processed by fetch_poolkey_data() method.""" + self.last_block = value + + async def fetch_poolkey_data( + self, + first_block: int, + chunk_size: int = 5, + clear_list: bool = True, + retry_attempts: int = 3, + minutes_between_retries: int = 3, + last_block: int | None = None, + ) -> int: + """ + :param first_block: Starting block for scanning process + :param chunk_size: Defines amount of blocks per single log request + :param clear_list: When True, clears pool list before log scanning, when False - new entries will be added to the end of the list. + :param retry_attempts: Number of attempts to retry and resume log retrieval in case of RPC returns errors like `500` etc. + :param minutes_between_retries: Minutes to wait between retry attempts. + :param last_block: Optional parameter defining the last block for scanning process. If None, current block number will be used. + :return: 0 if logs were successfully processed, -1 if logs retrieval failed (e.g. due to wrong chunk size or RPC endpoint failure). + """ + # Scans PoolManager contract' Initialize() event logs in order to get + # list of all pools. See documentation for suggested starting blocks. + # chunk_size default value 5 should be suitable for public RPCs; + # can be increased on paid or local RPCs for better performance. + + chain_id: int = int(await self.web3.net.version) + net_name = _netid_to_name[chain_id] + pool_manager_contract_address = _poolmanager_contract_addresses_v4[net_name] + pool_manager_contract = await _load_contract_async( + self.web3, + "uniswap-v4/poolmanager", + _str_to_addr(pool_manager_contract_address), + ) + first_block_number: int = first_block + if last_block is None: + last_block_number: int = int(await self.web3.eth.get_block_number()) + else: + last_block_number = min( + max(first_block, last_block), + int(await self.web3.eth.get_block_number()), + ) + + chunks_amount = int((last_block_number - first_block_number) // chunk_size) + start_block = first_block_number + end_block = 0 + + print( + f"Logs processing started, start block = {start_block}; end block = {last_block_number}." + ) + if clear_list: + self.poolkeys_list.clear() + retry_attempts_done: int = 0 + for i in range(chunks_amount + 1): + if start_block + chunk_size <= last_block_number: + end_block = start_block + chunk_size + else: + end_block = last_block_number + print( + f"Processing chunk {i}/{chunks_amount}; (start block = {start_block}; end block = {end_block})", + end="\r", + flush=True, + ) + try: + logs = await pool_manager_contract.events.Initialize().get_logs( + from_block=start_block, to_block=end_block + ) + except Exception as e: + # Exception occurs when chunk size value is too big so RPC endpoint rejects + # requests OR RPC endpoint has issues. + # In such cases, we will try to resume log retrieval process for a defined number of attempts. If all attempts fail, the method will be aborted and `-1`` value will be returned. + print(f"Error retrieving logs: {e}") + while retry_attempts_done < retry_attempts: + print() + print() + print( + f"Error retrieving logs. Retrying. ({retry_attempts_done + 1}/{retry_attempts})" + ) + print( + f"Waiting for {minutes_between_retries} minutes before next attempt..." + ) + await asyncio.sleep(int(minutes_between_retries) * 60) + retry_attempts_done += 1 + try: + logs = await pool_manager_contract.events.Initialize().get_logs( + from_block=start_block, to_block=end_block + ) + print("Issue addressed. Resuming log retrieval.") + retry_attempts_done = 0 + break + except Exception as e_reconnect: + print(f"Attempt {retry_attempts_done} failed: {e_reconnect}") + if retry_attempts_done == retry_attempts: + print( + "Couldn't retrieve logs; check chunk size and RPC availability. Aborted. " + ) + print(f"Error details: {e}") + return -1 + for log_item in logs: + try: + pool_currency0 = str(log_item.args.currency0) + pool_currency1 = str(log_item.args.currency1) + pool_fee = int(str(log_item.args.fee)) + pool_tick_spacing = int(str(log_item.args.tickSpacing)) + pool_hooks = str(log_item.args.hooks) + pool: PoolKey = PoolKey( + pool_currency0, + pool_currency1, + pool_fee, + pool_tick_spacing, + pool_hooks, + ) + if pool not in self.poolkeys_list: + self.poolkeys_list.append(pool) + except AttributeError as e: + print(f"Error occurred while processing log item: {e}") + continue + self.set_last_block(end_block) + if end_block == last_block_number: + break + start_block = start_block + chunk_size + + print( + "---------------------------------------------------------------------------------------------" + ) + print(f"Logs processing completed. Last block processed {last_block_number}") + self.set_last_block(last_block_number) + return 0 + + def save_poolkeys_list(self, poolkey_data_filename: str) -> None: + """Saves poolKey list to specified file (XML format)""" + pool_data = ET.Element("PoolData") + + for pool_item in self.poolkeys_list: + pool = ET.SubElement(pool_data, "Pool") + + currency0 = ET.SubElement(pool, "Currency0") + currency0.text = str(pool_item.currency0) + + currency1 = ET.SubElement(pool, "Currency1") + currency1.text = str(pool_item.currency1) + + fee = ET.SubElement(pool, "Fee") + fee.text = str(pool_item.fee) + + tick_spacing = ET.SubElement(pool, "TickSpacing") + tick_spacing.text = str(pool_item.tick_spacing) + + hooks = ET.SubElement(pool, "Hooks") + hooks.text = str(pool_item.hooks) + + ET.ElementTree(pool_data).write(poolkey_data_filename) + + def load_poolkeys_list(self, poolkey_data_filename: str) -> None: + """Loads poolKey list from specified file (XML format)""" + if os.path.isfile(poolkey_data_filename): + try: + tree = ET.parse(poolkey_data_filename) + except ET.ParseError: + raise ValueError( + "Parse error, file seems to be corrupted (" + + poolkey_data_filename + + ")" + ) + self.poolkeys_list.clear() + root = tree.getroot() + for item in root: + pool_currency0 = str(item[0].text) + pool_currency1 = str(item[1].text) + pool_fee = int(str(item[2].text)) + pool_tick_spacing = int(str(item[3].text)) + pool_hooks = str(item[4].text) + pool: PoolKey = PoolKey( + pool_currency0, + pool_currency1, + pool_fee, + pool_tick_spacing, + pool_hooks, + ) + self.poolkeys_list.append(pool) + else: + raise ValueError("Couldn't locate file " + poolkey_data_filename) + + def get_poolkeys_sublist(self, currency0: str, currency1: str) -> list[PoolKey]: + """Returns all pools for the (currency0, currency1) pair""" + if currency0.lower() < currency1.lower(): + c0, c1 = currency0.lower(), currency1.lower() + else: + c0, c1 = currency1.lower(), currency0.lower() + result_list = [ + x + for x in self.poolkeys_list + if c0 == x.currency0.lower() and c1 == x.currency1.lower() + ] + return result_list