diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 106d5922..0257f754 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,4 @@ -# 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: @@ -10,12 +7,17 @@ on: branches: [ master ] jobs: - build: + test: + name: test (v${{ matrix.uniswap-version }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + uniswap-version: [1, 2, 3] steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python uses: actions/setup-python@v1 with: python-version: 3.8 @@ -23,17 +25,72 @@ jobs: uses: actions/setup-node@v2-beta with: node-version: '12' - - name: Install dependencies + + # 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: | + echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info[:3]))") + - 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@v2 + 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: | poetry install npm install -g ganache-cli - - name: Typecheck - run: | - make typecheck - name: Test env: PROVIDER: ${{ secrets.MAINNET_PROVIDER }} MAINNET_PROVIDER: ${{ secrets.MAINNET_PROVIDER }} + UNISWAP_VERSION: ${{ matrix.uniswap-version }} run: | make test + + typecheck: + name: typecheck + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + # 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: | + echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info[:3]))") + - 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@v2 + 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/Makefile b/Makefile index fbe4bb39..29b2775f 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,12 @@ typecheck: lint: poetry run flake8 +format: + black uniswap + +format-abis: + npx prettier --write --parser=json uniswap/assets/*/*.abi + precommit: make typecheck make lint diff --git a/poetry.lock b/poetry.lock index 95031eb2..1ddfd3ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -24,17 +24,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "20.3.0" +version = "21.2.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"] -docs = ["furo", "sphinx", "zope.interface"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] name = "babel" @@ -141,6 +141,9 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +[package.dependencies] +toml = {version = "*", optional = true, markers = "extra == \"toml\""} + [package.extras] toml = ["toml"] @@ -318,7 +321,7 @@ test = ["hypothesis (>=4.43.0,<5.0.0)", "pytest (==5.4.1)", "pytest-xdist", "tox [[package]] name = "flake8" -version = "3.9.1" +version = "3.9.2" description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false @@ -390,17 +393,17 @@ requests = ">=2.11" [[package]] name = "jinja2" -version = "2.11.3" +version = "3.0.0" description = "A very fast and expressive template engine." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.6" [package.dependencies] -MarkupSafe = ">=0.23" +MarkupSafe = ">=2.0.0rc2" [package.extras] -i18n = ["Babel (>=0.8)"] +i18n = ["Babel (>=2.7)"] [[package]] name = "jsonschema" @@ -430,11 +433,11 @@ python-versions = "*" [[package]] name = "markupsafe" -version = "1.1.1" +version = "2.0.0" description = "Safely add untrusted strings to HTML/XML markup." category = "dev" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = ">=3.6" [[package]] name = "mccabe" @@ -468,7 +471,7 @@ varint = "*" [[package]] name = "mypy" -version = "0.782" +version = "0.812" description = "Optional static typing for Python" category = "dev" optional = false @@ -544,7 +547,7 @@ dev = ["pre-commit", "tox"] [[package]] name = "protobuf" -version = "3.15.8" +version = "3.17.0" description = "Protocol Buffers" category = "main" optional = false @@ -662,14 +665,14 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm [[package]] name = "pytest-cov" -version = "2.11.1" +version = "2.12.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.dependencies] -coverage = ">=5.2.1" +coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] @@ -768,7 +771,7 @@ test = ["hypothesis (==5.19.0)", "pytest (==5.4.3)", "tox (>=2.9.1,<3)"] [[package]] name = "six" -version = "1.15.0" +version = "1.16.0" description = "Python 2 and 3 compatibility utilities" category = "main" optional = false @@ -1041,7 +1044,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "c5a4beca325cbc6b09255ea1ca6d547a35412afe0484f44751f05cb4683bee8e" +content-hash = "5e41971b3787b291bf86212cd2dd95f336056c80630666f65e9f9cf14ca8525e" [metadata.files] alabaster = [ @@ -1057,8 +1060,8 @@ atomicwrites = [ {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ - {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"}, - {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"}, + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] babel = [ {file = "Babel-2.9.1-py2.py3-none-any.whl", hash = "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9"}, @@ -1190,8 +1193,8 @@ eth-utils = [ {file = "eth_utils-1.10.0-py3-none-any.whl", hash = "sha256:74240a8c6f652d085ed3c85f5f1654203d2f10ff9062f83b3bad0a12ff321c7a"}, ] flake8 = [ - {file = "flake8-3.9.1-py2.py3-none-any.whl", hash = "sha256:3b9f848952dddccf635be78098ca75010f073bfe14d2c6bda867154bea728d2a"}, - {file = "flake8-3.9.1.tar.gz", hash = "sha256:1aa8990be1e689d96c745c5682b687ea49f2e05a443aff1f8251092b0014e378"}, + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] hexbytes = [ {file = "hexbytes-0.2.1-py3-none-any.whl", hash = "sha256:a093a5533aa63ca6614246fa97feb693b5813f9e736c38b68fe4e2d8fcc35aa5"}, @@ -1214,8 +1217,8 @@ ipfshttpclient = [ {file = "ipfshttpclient-0.6.1.tar.gz", hash = "sha256:fb2b12b249dd45c4110b946a16c03ec1eeba6789791bd425e678d93c3f631098"}, ] jinja2 = [ - {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, - {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, + {file = "Jinja2-3.0.0-py3-none-any.whl", hash = "sha256:2f2de5285cf37f33d33ecd4a9080b75c87cd0c1994d5a9c6df17131ea1f049c6"}, + {file = "Jinja2-3.0.0.tar.gz", hash = "sha256:ea8d7dd814ce9df6de6a761ec7f1cac98afe305b8cdc4aaae4e114b8d8ce24c5"}, ] jsonschema = [ {file = "jsonschema-3.2.0-py2.py3-none-any.whl", hash = "sha256:4e5b3cf8216f577bee9ce139cbe72eca3ea4f292ec60928ff24758ce626cd163"}, @@ -1225,58 +1228,40 @@ lru-dict = [ {file = "lru-dict-1.1.7.tar.gz", hash = "sha256:45b81f67d75341d4433abade799a47e9c42a9e22a118531dcb5e549864032d7c"}, ] markupsafe = [ - {file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win32.whl", hash = "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b"}, - {file = "MarkupSafe-1.1.1-cp27-cp27m-win_amd64.whl", hash = "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f"}, - {file = "MarkupSafe-1.1.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-macosx_10_6_intel.whl", hash = "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win32.whl", hash = "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21"}, - {file = "MarkupSafe-1.1.1-cp34-cp34m-win_amd64.whl", hash = "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-macosx_10_6_intel.whl", hash = "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, - {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, - {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, + {file = "MarkupSafe-2.0.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2efaeb1baff547063bad2b2893a8f5e9c459c4624e1a96644bbba08910ae34e0"}, + {file = "MarkupSafe-2.0.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:441ce2a8c17683d97e06447fcbccbdb057cbf587c78eb75ae43ea7858042fe2c"}, + {file = "MarkupSafe-2.0.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:45535241baa0fc0ba2a43961a1ac7562ca3257f46c4c3e9c0de38b722be41bd1"}, + {file = "MarkupSafe-2.0.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:90053234a6479738fd40d155268af631c7fca33365f964f2208867da1349294b"}, + {file = "MarkupSafe-2.0.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3b54a9c68995ef4164567e2cd1a5e16db5dac30b2a50c39c82db8d4afaf14f63"}, + {file = "MarkupSafe-2.0.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:f58b5ba13a5689ca8317b98439fccfbcc673acaaf8241c1869ceea40f5d585bf"}, + {file = "MarkupSafe-2.0.0-cp36-cp36m-win32.whl", hash = "sha256:a00dce2d96587651ef4fa192c17e039e8cfab63087c67e7d263a5533c7dad715"}, + {file = "MarkupSafe-2.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:007dc055dbce5b1104876acee177dbfd18757e19d562cd440182e1f492e96b95"}, + {file = "MarkupSafe-2.0.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a08cd07d3c3c17cd33d9e66ea9dee8f8fc1c48e2d11bd88fd2dc515a602c709b"}, + {file = "MarkupSafe-2.0.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3c352ff634e289061711608f5e474ec38dbaa21e3e168820d53d5f4015e5b91b"}, + {file = "MarkupSafe-2.0.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:32200f562daaab472921a11cbb63780f1654552ae49518196fc361ed8e12e901"}, + {file = "MarkupSafe-2.0.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:fef86115fdad7ae774720d7103aa776144cf9b66673b4afa9bcaa7af990ed07b"}, + {file = "MarkupSafe-2.0.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:e79212d09fc0e224d20b43ad44bb0a0a3416d1e04cf6b45fed265114a5d43d20"}, + {file = "MarkupSafe-2.0.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:79b2ae94fa991be023832e6bcc00f41dbc8e5fe9d997a02db965831402551730"}, + {file = "MarkupSafe-2.0.0-cp37-cp37m-win32.whl", hash = "sha256:3261fae28155e5c8634dd7710635fe540a05b58f160cef7713c7700cb9980e66"}, + {file = "MarkupSafe-2.0.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e4570d16f88c7f3032ed909dc9e905a17da14a1c4cfd92608e3fda4cb1208bbd"}, + {file = "MarkupSafe-2.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8f806bfd0f218477d7c46a11d3e52dc7f5fdfaa981b18202b7dc84bbc287463b"}, + {file = "MarkupSafe-2.0.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e77e4b983e2441aff0c0d07ee711110c106b625f440292dfe02a2f60c8218bd6"}, + {file = "MarkupSafe-2.0.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:031bf79a27d1c42f69c276d6221172417b47cb4b31cdc73d362a9bf5a1889b9f"}, + {file = "MarkupSafe-2.0.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:83cf0228b2f694dcdba1374d5312f2277269d798e65f40344964f642935feac1"}, + {file = "MarkupSafe-2.0.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:4cc563836f13c57f1473bc02d1e01fc37bab70ad4ee6be297d58c1d66bc819bf"}, + {file = "MarkupSafe-2.0.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:d00a669e4a5bec3ee6dbeeeedd82a405ced19f8aeefb109a012ea88a45afff96"}, + {file = "MarkupSafe-2.0.0-cp38-cp38-win32.whl", hash = "sha256:161d575fa49395860b75da5135162481768b11208490d5a2143ae6785123e77d"}, + {file = "MarkupSafe-2.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:58bc9fce3e1557d463ef5cee05391a05745fd95ed660f23c1742c711712c0abb"}, + {file = "MarkupSafe-2.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3fb47f97f1d338b943126e90b79cad50d4fcfa0b80637b5a9f468941dbbd9ce5"}, + {file = "MarkupSafe-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dab0c685f21f4a6c95bfc2afd1e7eae0033b403dd3d8c1b6d13a652ada75b348"}, + {file = "MarkupSafe-2.0.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:664832fb88b8162268928df233f4b12a144a0c78b01d38b81bdcf0fc96668ecb"}, + {file = "MarkupSafe-2.0.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:df561f65049ed3556e5b52541669310e88713fdae2934845ec3606f283337958"}, + {file = "MarkupSafe-2.0.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:24bbc3507fb6dfff663af7900a631f2aca90d5a445f272db5fc84999fa5718bc"}, + {file = "MarkupSafe-2.0.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:87de598edfa2230ff274c4de7fcf24c73ffd96208c8e1912d5d0fee459767d75"}, + {file = "MarkupSafe-2.0.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:a19d39b02a24d3082856a5b06490b714a9d4179321225bbf22809ff1e1887cc8"}, + {file = "MarkupSafe-2.0.0-cp39-cp39-win32.whl", hash = "sha256:4aca81a687975b35e3e80bcf9aa93fe10cd57fac37bf18b2314c186095f57e05"}, + {file = "MarkupSafe-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:70820a1c96311e02449591cbdf5cd1c6a34d5194d5b55094ab725364375c9eb2"}, + {file = "MarkupSafe-2.0.0.tar.gz", hash = "sha256:4fae0677f712ee090721d8b17f412f1cbceefbf0dc180fe91bab3232f38b4527"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, @@ -1291,20 +1276,28 @@ multiaddr = [ {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"}, + {file = "mypy-0.812-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a26f8ec704e5a7423c8824d425086705e381b4f1dfdef6e3a1edab7ba174ec49"}, + {file = "mypy-0.812-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:28fb5479c494b1bab244620685e2eb3c3f988d71fd5d64cc753195e8ed53df7c"}, + {file = "mypy-0.812-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:9743c91088d396c1a5a3c9978354b61b0382b4e3c440ce83cf77994a43e8c521"}, + {file = "mypy-0.812-cp35-cp35m-win_amd64.whl", hash = "sha256:d7da2e1d5f558c37d6e8c1246f1aec1e7349e4913d8fb3cb289a35de573fe2eb"}, + {file = "mypy-0.812-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:4eec37370483331d13514c3f55f446fc5248d6373e7029a29ecb7b7494851e7a"}, + {file = "mypy-0.812-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d65cc1df038ef55a99e617431f0553cd77763869eebdf9042403e16089fe746c"}, + {file = "mypy-0.812-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:61a3d5b97955422964be6b3baf05ff2ce7f26f52c85dd88db11d5e03e146a3a6"}, + {file = "mypy-0.812-cp36-cp36m-win_amd64.whl", hash = "sha256:25adde9b862f8f9aac9d2d11971f226bd4c8fbaa89fb76bdadb267ef22d10064"}, + {file = "mypy-0.812-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:552a815579aa1e995f39fd05dde6cd378e191b063f031f2acfe73ce9fb7f9e56"}, + {file = "mypy-0.812-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:499c798053cdebcaa916eef8cd733e5584b5909f789de856b482cd7d069bdad8"}, + {file = "mypy-0.812-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:5873888fff1c7cf5b71efbe80e0e73153fe9212fafdf8e44adfe4c20ec9f82d7"}, + {file = "mypy-0.812-cp37-cp37m-win_amd64.whl", hash = "sha256:9f94aac67a2045ec719ffe6111df543bac7874cee01f41928f6969756e030564"}, + {file = "mypy-0.812-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d23e0ea196702d918b60c8288561e722bf437d82cb7ef2edcd98cfa38905d506"}, + {file = "mypy-0.812-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:674e822aa665b9fd75130c6c5f5ed9564a38c6cea6a6432ce47eafb68ee578c5"}, + {file = "mypy-0.812-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:abf7e0c3cf117c44d9285cc6128856106183938c68fd4944763003decdcfeb66"}, + {file = "mypy-0.812-cp38-cp38-win_amd64.whl", hash = "sha256:0d0a87c0e7e3a9becdfbe936c981d32e5ee0ccda3e0f07e1ef2c3d1a817cf73e"}, + {file = "mypy-0.812-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7ce3175801d0ae5fdfa79b4f0cfed08807af4d075b402b7e294e6aa72af9aa2a"}, + {file = "mypy-0.812-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:b09669bcda124e83708f34a94606e01b614fa71931d356c1f1a5297ba11f110a"}, + {file = "mypy-0.812-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:33f159443db0829d16f0a8d83d94df3109bb6dd801975fe86bacb9bf71628e97"}, + {file = "mypy-0.812-cp39-cp39-win_amd64.whl", hash = "sha256:3f2aca7f68580dc2508289c729bd49ee929a436208d2b2b6aab15745a70a57df"}, + {file = "mypy-0.812-py3-none-any.whl", hash = "sha256:2f9b3407c58347a452fc0736861593e105139b905cca7d097e413453a1d650b4"}, + {file = "mypy-0.812.tar.gz", hash = "sha256:cd07039aa5df222037005b08fbbfd69b3ab0b0bd7a07d7906de75ae52c4e3119"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -1330,26 +1323,29 @@ pluggy = [ {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] protobuf = [ - {file = "protobuf-3.15.8-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:fad4f971ec38d8df7f4b632c819bf9bbf4f57cfd7312cf526c69ce17ef32436a"}, - {file = "protobuf-3.15.8-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:f17b352d7ce33c81773cf81d536ca70849de6f73c96413f17309f4b43ae7040b"}, - {file = "protobuf-3.15.8-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:4a054b0b5900b7ea7014099e783fb8c4618e4209fffcd6050857517b3f156e18"}, - {file = "protobuf-3.15.8-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:efa4c4d4fc9ba734e5e85eaced70e1b63fb3c8d08482d839eb838566346f1737"}, - {file = "protobuf-3.15.8-cp35-cp35m-win32.whl", hash = "sha256:07eec4e2ccbc74e95bb9b3afe7da67957947ee95bdac2b2e91b038b832dd71f0"}, - {file = "protobuf-3.15.8-cp35-cp35m-win_amd64.whl", hash = "sha256:f9cadaaa4065d5dd4d15245c3b68b967b3652a3108e77f292b58b8c35114b56c"}, - {file = "protobuf-3.15.8-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2dc0e8a9e4962207bdc46a365b63a3f1aca6f9681a5082a326c5837ef8f4b745"}, - {file = "protobuf-3.15.8-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:f80afc0a0ba13339bbab25ca0409e9e2836b12bb012364c06e97c2df250c3343"}, - {file = "protobuf-3.15.8-cp36-cp36m-win32.whl", hash = "sha256:c5566f956a26cda3abdfacc0ca2e21db6c9f3d18f47d8d4751f2209d6c1a5297"}, - {file = "protobuf-3.15.8-cp36-cp36m-win_amd64.whl", hash = "sha256:dab75b56a12b1ceb3e40808b5bd9dfdaef3a1330251956e6744e5b6ed8f8830b"}, - {file = "protobuf-3.15.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3053f13207e7f13dc7be5e9071b59b02020172f09f648e85dc77e3fcb50d1044"}, - {file = "protobuf-3.15.8-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:1f0b5d156c3df08cc54bc2c8b8b875648ea4cd7ebb2a9a130669f7547ec3488c"}, - {file = "protobuf-3.15.8-cp37-cp37m-win32.whl", hash = "sha256:90270fe5732c1f1ff664a3bd7123a16456d69b4e66a09a139a00443a32f210b8"}, - {file = "protobuf-3.15.8-cp37-cp37m-win_amd64.whl", hash = "sha256:f42c2f5fb67da5905bfc03733a311f72fa309252bcd77c32d1462a1ad519521e"}, - {file = "protobuf-3.15.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6077db37bfa16494dca58a4a02bfdacd87662247ad6bc1f7f8d13ff3f0013e1"}, - {file = "protobuf-3.15.8-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:510e66491f1a5ac5953c908aa8300ec47f793130097e4557482803b187a8ee05"}, - {file = "protobuf-3.15.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5ff9fa0e67fcab442af9bc8d4ec3f82cb2ff3be0af62dba047ed4187f0088b7d"}, - {file = "protobuf-3.15.8-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:1c0e9e56202b9dccbc094353285a252e2b7940b74fdf75f1b4e1b137833fabd7"}, - {file = "protobuf-3.15.8-py2.py3-none-any.whl", hash = "sha256:a0a08c6b2e6d6c74a6eb5bf6184968eefb1569279e78714e239d33126e753403"}, - {file = "protobuf-3.15.8.tar.gz", hash = "sha256:0277f62b1e42210cafe79a71628c1d553348da81cbd553402a7f7549c50b11d0"}, + {file = "protobuf-3.17.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:15351df904347da2081a2eebc42b192c29724eb57dbe56dae440be843f1e4779"}, + {file = "protobuf-3.17.0-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:5356981c1919782b8c2e3ea5c5d85ad5937b8178a025ac9edc2f2ca5b4a717ae"}, + {file = "protobuf-3.17.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:eac0a2a7ea99e17175f6e7b53cdc9004ed786c072fbdf933def0e454e14fd323"}, + {file = "protobuf-3.17.0-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4c8d0997fdc0a4cf9de7950d598ce6974b22e8618bbcf1d15e9842010cf8420a"}, + {file = "protobuf-3.17.0-cp35-cp35m-win32.whl", hash = "sha256:9ae321459d4890c3939c536382f75e232c9e91ce506310353c8a15ad5c379e0d"}, + {file = "protobuf-3.17.0-cp35-cp35m-win_amd64.whl", hash = "sha256:295944ef0772498d7bf75f6aa5d4dfcfd02f5ce70f735b406e52e43ac3914d38"}, + {file = "protobuf-3.17.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:850f429bd2399525d339d05bc809f090f16d3d88737bed637d355a5ee8d3b81a"}, + {file = "protobuf-3.17.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:809a96d5a1a74538728710f9104f43ae77f5e48bde274ee321b10a324ba52e4f"}, + {file = "protobuf-3.17.0-cp36-cp36m-win32.whl", hash = "sha256:8a3ac375539055164f31a330770f137875307e6f04c21e2647f2e7139c501295"}, + {file = "protobuf-3.17.0-cp36-cp36m-win_amd64.whl", hash = "sha256:3d338910b10b88b18581cf6877b3938b2e262e8fdc2c1057f5a291787de63183"}, + {file = "protobuf-3.17.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1488f786bd1912f97796cf5def8cacf433735616896cf7ed9dc786cee693dfc8"}, + {file = "protobuf-3.17.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:bcaff977db178f0bfde10bab0d23a5f5adf5964adba70c315e45922a1c55eb90"}, + {file = "protobuf-3.17.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:939ce06846ddfec99c0bff510510b3ee45778e7a3aec6544d1f36526e5fecb67"}, + {file = "protobuf-3.17.0-cp37-cp37m-win32.whl", hash = "sha256:3237acce5b666c7b0f45785cc2d0809796d4df3593bd68338aebf25408139188"}, + {file = "protobuf-3.17.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2f77afe33bb86c7d34221a86193256d69aa10818620fe4a7513d98211d67d672"}, + {file = "protobuf-3.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:acc9f2091ace3de429eee424ab7ba0bc52a6aa9ffc9909e5c4de259a3f71db46"}, + {file = "protobuf-3.17.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a29631f4f8bcf79b12a59e83d238d888de5034871461d788c74c68218ad75049"}, + {file = "protobuf-3.17.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:05c304396e309661c45e3a97bd2d8da1fc2bab743ed2ca880bcb757271c40c0e"}, + {file = "protobuf-3.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:baea44967071e6a51e705e4e88aebf35f530a14004cc69f60a185e5d7e13de7e"}, + {file = "protobuf-3.17.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:3b5c461af5a3cebd796c73370db929b7e24cbaba655eefdc044226bc8a843d6b"}, + {file = "protobuf-3.17.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44399393c3a8cc04a4cfbdc721dd7f2114497efda582e946a91b8c4290ae5ff5"}, + {file = "protobuf-3.17.0-py2.py3-none-any.whl", hash = "sha256:e32ef0c9f4b548c80d94dfff8b4130ca2ff3d50caaf2455889e3f5b8a01e8038"}, + {file = "protobuf-3.17.0.tar.gz", hash = "sha256:05dfe9319939a8473c21b469f34f6486646e54fb8542637cf7ed8e2fbfe21538"}, ] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, @@ -1419,8 +1415,8 @@ pytest = [ {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, ] pytest-cov = [ - {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"}, - {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"}, + {file = "pytest-cov-2.12.0.tar.gz", hash = "sha256:8535764137fecce504a49c2b742288e3d34bc09eed298ad65963616cc98fd45e"}, + {file = "pytest_cov-2.12.0-py2.py3-none-any.whl", hash = "sha256:95d4933dcbbacfa377bb60b29801daa30d90c33981ab2a79e9ab4452c165066e"}, ] pytest-dotenv = [ {file = "pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732"}, @@ -1529,8 +1525,8 @@ rlp = [ {file = "rlp-2.0.1.tar.gz", hash = "sha256:665e8312750b3fc5f7002e656d05b9dcb6e93b6063df40d95c49ad90c19d1f0e"}, ] six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] snowballstemmer = [ {file = "snowballstemmer-2.1.0-py2.py3-none-any.whl", hash = "sha256:b51b447bea85f9968c13b650126a888aabd4cb4463fca868ec596826325dedc2"}, diff --git a/pyproject.toml b/pyproject.toml index ce670656..0f275e00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,13 +28,13 @@ web3 = "^5.12.0" click = "^7.1.2" [tool.poetry.dev-dependencies] -pytest = "^5.4.3" +mypy = "*" black = "^19.10b0" +pytest = "^5.4.3" pytest-cov = "^2.10.0" pytest-dotenv = "^0.5.2" python-dotenv = "^0.14.0" flake8 = "^3.8.3" -mypy = "^0.782" Sphinx = "^3.5.4" sphinx-book-theme = "^0.1.0" sphinx-click = "^2.7.1" diff --git a/tests/test_cli.py b/tests/test_cli.py index a830b7e2..825f5762 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,35 +1,46 @@ import json +import sys from click.testing import CliRunner from uniswap.cli import main +def print_result(result): + print(result) + print(result.stdout.strip()) + print(result.stderr.strip(), file=sys.stderr) + + def test_get_price(): - runner = CliRunner() + runner = CliRunner(mix_stderr=False) result = runner.invoke(main, ["price", "weth", "dai"]) + print_result(result) assert result.exit_code == 0 # Will break when ETH breaks 10k - assert 1000 < float(result.output) < 10_000 + assert 1000 < float(result.stdout) < 10_000 result = runner.invoke(main, ["price", "wbtc", "dai"]) assert result.exit_code == 0 # Will break when BTC breaks 100k - assert 10_000 < float(result.output) < 100_000 + assert 10_000 < float(result.stdout) < 100_000 def test_get_token(): - runner = CliRunner() + runner = CliRunner(mix_stderr=False) result = runner.invoke(main, ["token", "weth"]) + print_result(result) assert result.exit_code == 0 - out = json.loads(result.output.replace("'", '"')) + + out = json.loads(result.stdout.replace("'", '"')) assert out["symbol"] == "WETH" assert out["decimals"] == 18 def test_get_tokendb(): - runner = CliRunner() + 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 78fed1b2..ee70f60e 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -2,6 +2,7 @@ import os import subprocess import shutil +import logging from typing import Generator from contextlib import contextmanager from dataclasses import dataclass @@ -12,6 +13,15 @@ from uniswap import Uniswap, InvalidToken, InsufficientBalance +logger = logging.getLogger(__name__) + +ENV_UNISWAP_VERSION = os.getenv("UNISWAP_VERSION", None) +if ENV_UNISWAP_VERSION: + UNISWAP_VERSIONS = [int(ENV_UNISWAP_VERSION)] +else: + UNISWAP_VERSIONS = [1, 2, 3] + + @dataclass class GanacheInstance: provider: str @@ -19,13 +29,28 @@ class GanacheInstance: eth_privkey: str -@pytest.fixture(scope="module", params=[1, 2]) +@pytest.fixture(scope="module", params=UNISWAP_VERSIONS) def client(request, web3: Web3, ganache: GanacheInstance): - uniswap = Uniswap( + return Uniswap( ganache.eth_address, ganache.eth_privkey, web3=web3, version=request.param ) - uniswap._buy_test_assets() - return uniswap + + +@pytest.fixture(scope="module") +def test_assets(client: Uniswap): + """ + Buy some DAI and USDC to test with. + """ + tokens = client._get_token_addresses() + + for token_name, amount in [("DAI", 100 * 10 ** 18), ("USDC", 100 * 10 ** 6)]: + token_addr = tokens[token_name] + price = client.get_eth_token_output_price(token_addr, amount) + logger.info(f"Cost of {amount} {token_name}: {price}") + logger.info("Buying...") + + tx = client.make_trade_output(tokens["ETH"], token_addr, amount) + client.w3.eth.waitForTransactionReceipt(tx) @pytest.fixture(scope="module") @@ -78,6 +103,7 @@ class TestUniswap(object): weth = Web3.toChecksumAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") bat = Web3.toChecksumAddress("0x0D8775F648430679A709E98d2b0Cb6250d2887EF") dai = Web3.toChecksumAddress("0x6b175474e89094c44da98b954eedeac495271d0f") + usdc = Web3.toChecksumAddress("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48") # For Rinkeby # eth = "0x0000000000000000000000000000000000000000" @@ -86,10 +112,14 @@ class TestUniswap(object): # ------ 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 @@ -121,19 +151,21 @@ def test_get_token_eth_input_price(self, client, token, qty): assert r @pytest.mark.parametrize( - "token0, token1, qty", + "token0, token1, qty, kwargs", [ - (bat, dai, ONE_ETH), - (dai, bat, ONE_ETH), - (bat, dai, 2 * ONE_ETH), - (weth, dai, ONE_ETH), - (dai, weth, ONE_ETH), + # BAT/DAI has no liquidity in V3 + # (bat, dai, ONE_ETH), + # (dai, bat, ONE_ETH), + # (bat, dai, 2 * ONE_ETH), + (dai, usdc, ONE_ETH, {"fee": 500}), + (weth, dai, ONE_ETH, {}), + (dai, weth, ONE_ETH, {}), ], ) - 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_token_token_input_price(self, client, token0, token1, qty, kwargs): + if client.version not in [2, 3]: + pytest.skip("Tested method not supported in this Uniswap version") + r = client.get_token_token_input_price(token0, token1, qty, **kwargs) assert r @pytest.mark.parametrize( @@ -163,19 +195,19 @@ def test_get_token_eth_output_price(self, client, token, qty): assert r @pytest.mark.parametrize( - "token0, token1, qty", + "token0, token1, qty, kwargs", [ - (bat, dai, ONE_ETH), - (dai, bat, ONE_ETH), - (bat, dai, 2 * ONE_ETH), - (weth, dai, ONE_ETH), - (dai, weth, ONE_ETH), + # (bat, dai, ONE_ETH), + (dai, usdc, ONE_ETH, {"fee": 500}), + # (bat, dai, 2 * ONE_ETH), + (weth, dai, ONE_ETH, {}), + (dai, weth, ONE_ETH, {}), ], ) - 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_token_token_output_price(self, client, token0, token1, qty, kwargs): + if client.version not in [2, 3]: + pytest.skip("Tested method not supported in this Uniswap version") + r = client.get_token_token_output_price(token0, token1, qty, **kwargs) assert r # ------ ERC20 Pool ---------------------------------------------------------------- @@ -241,11 +273,11 @@ def test_remove_liquidity( "input_token, output_token, qty, recipient, expectation", [ # ETH -> Token - (eth, bat, 1_000_000_000 * ONE_WEI, None, does_not_raise), + (eth, dai, 1_000_000_000 * ONE_WEI, None, does_not_raise), # Token -> Token - (bat, dai, 1_000_000_000 * ONE_WEI, None, does_not_raise), + (dai, usdc, 1_000_000_000 * ONE_WEI, None, does_not_raise), # Token -> ETH - (bat, eth, 1_000_000 * ONE_WEI, None, does_not_raise), + (usdc, 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), @@ -256,6 +288,7 @@ def test_make_trade( self, client: Uniswap, web3: Web3, + test_assets, input_token, output_token, qty: int, @@ -278,9 +311,9 @@ def test_make_trade( "input_token, output_token, qty, recipient, expectation", [ # ETH -> Token - (eth, bat, 1_000_000_000 * ONE_WEI, None, does_not_raise), + (eth, dai, 1_000_000 * ONE_WEI, None, does_not_raise), # Token -> Token - (bat, dai, 1_000_000_000 * ONE_WEI, None, does_not_raise), + (dai, usdc, 1_000_000 * ONE_WEI, None, does_not_raise), # Token -> ETH (dai, eth, 1_000_000 * ONE_WEI, None, does_not_raise), # FIXME: These should probably be uncommented eventually @@ -301,6 +334,7 @@ def test_make_trade_output( self, client: Uniswap, web3: Web3, + test_assets, input_token, output_token, qty: int, diff --git a/uniswap/__init__.py b/uniswap/__init__.py index 42f05674..7dc75c54 100644 --- a/uniswap/__init__.py +++ b/uniswap/__init__.py @@ -1,2 +1,2 @@ -from .uniswap import Uniswap, InvalidToken, InsufficientBalance +from .uniswap import Uniswap, InvalidToken, InsufficientBalance, _str_to_addr from .cli import main 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/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/cli.py b/uniswap/cli.py index b94be02e..47d13c4f 100644 --- a/uniswap/cli.py +++ b/uniswap/cli.py @@ -1,4 +1,5 @@ import logging +import os import click from dotenv import load_dotenv @@ -28,7 +29,11 @@ def _coerce_to_checksum(addr: str) -> str: @click.group() @click.option("-v", "--verbose", is_flag=True) -@click.option("--version", type=click.Choice(["1", "2"]), default="2") +@click.option( + "--version", + type=click.Choice(["1", "2", "3"]), + default=os.getenv("UNISWAP_VERSION", "2"), +) @click.pass_context def main(ctx: click.Context, verbose: bool, version: str) -> None: logging.basicConfig(level=logging.INFO if verbose else logging.WARNING) @@ -88,7 +93,7 @@ def tokendb(ctx: click.Context, metadata: bool) -> None: """List known token addresses""" uni = ctx.obj["UNISWAP"] for symbol, addr in tokens.items(): - if metadata: + if metadata and addr != "0x0000000000000000000000000000000000000000": data = uni.get_token(_str_to_addr(addr)) data["address"] = addr assert data["symbol"].lower() == symbol.lower() diff --git a/uniswap/py.typed b/uniswap/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/uniswap/tokens.py b/uniswap/tokens.py index e51d0ba4..ed5ee60b 100644 --- a/uniswap/tokens.py +++ b/uniswap/tokens.py @@ -1,7 +1,27 @@ -tokens = { - "WETH": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", - "DAI": "0x6B175474E89094C44Da98b954EedeAC495271d0F", - "BAT": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", - "WBTC": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", - "UNI": "0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984", +from typing import Dict + +from web3 import Web3 +from web3.types import ChecksumAddress + + +tokens: Dict[str, ChecksumAddress] = { + k: Web3.toChecksumAddress(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.toChecksumAddress(v) + for k, v in { + "ETH": "0x0000000000000000000000000000000000000000", + "DAI": "0x2448eE2641d78CC42D7AD76498917359D961A783", + "BAT": "0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B", + }.items() } diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 1e8ce7e4..40857cb6 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -20,7 +20,10 @@ from eth_utils import is_same_address from eth_typing import AnyAddress +from .tokens import tokens, tokens_rinkeby + ETH_ADDRESS = "0x0000000000000000000000000000000000000000" +WETH9_ADDRESS = Web3.toChecksumAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") logger = logging.getLogger(__name__) @@ -67,12 +70,16 @@ def approved(self: Any, *args: Any, **kwargs: Any) -> Any: # 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) + logger.warning(f"Approved {token}") if token_two: is_approved = self._is_approved(token_two) + logger.warning(f"Approved {token_two}: {is_approved}") if not is_approved: self.approve(token_two) + logger.warning(f"Approved {token_two}") return method(self, *args, **kwargs) return approved @@ -92,7 +99,7 @@ def g(f: Callable) -> Callable: 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" + f"Function {f.__name__} does not support version {self.version} of Uniswap passed to constructor" ) return f(self, *args, **kwargs) @@ -101,13 +108,17 @@ def check_version(self: "Uniswap", *args: List, **kwargs: Dict) -> Any: 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) +def _str_to_addr(s: Union[str, Address]) -> AddressLike: + """Idempotent""" + if isinstance(s, str): + if s.startswith("0x"): + return Address(bytes.fromhex(s[2:])) + elif s.endswith(".eth"): + return ENS(s) + else: + raise Exception(f"Couldn't convert string '{s}' to AddressLike") else: - raise Exception(f"Couldn't convert string '{s}' to AddressLike") + return s def _addr_to_str(a: AddressLike) -> str: @@ -194,20 +205,13 @@ def __init__( :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). """ - if address is None: - self.address: AddressLike = _str_to_addr( - "0x0000000000000000000000000000000000000000" - ) - else: - self.address = ( - _str_to_addr(address) if isinstance(address, str) else address - ) - if private_key is None: - self.private_key = ( - "0x0000000000000000000000000000000000000000000000000000000000000000" - ) - else: - self.private_key = private_key + self.address: AddressLike = _str_to_addr( + address or "0x0000000000000000000000000000000000000000" + ) + self.private_key = ( + private_key + or "0x0000000000000000000000000000000000000000000000000000000000000000" + ) self.version = version @@ -265,10 +269,23 @@ def __init__( self.router = self._load_contract( abi_name="uniswap-v2/router02", address=self.router_address, ) + elif self.version == 3: + # https://github.com/Uniswap/uniswap-v3-periphery/blob/main/deploys.md + quoter_addr = _str_to_addr("0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6") + self.router_address = _str_to_addr( + "0xE592427A0AEce92De3Edee1F18E0157C05861564" + ) + self.quoter = self._load_contract( + abi_name="uniswap-v3/quoter", address=quoter_addr + ) + self.router = self._load_contract( + abi_name="uniswap-v3/router", address=self.router_address + ) else: raise Exception(f"Invalid version '{self.version}', only 1 or 2 supported") - logger.info(f"Using factory contract: {self.factory_contract}") + if hasattr(self, "factory_contract"): + logger.info(f"Using factory contract: {self.factory_contract}") @supports([1]) def get_all_tokens(self) -> List[dict]: @@ -344,10 +361,13 @@ def erc20_contract(self, token_addr: AddressLike) -> Contract: return self._load_contract(abi_name="erc20", address=token_addr) @functools.lru_cache() - @supports([2]) + @supports([2, 3]) def get_weth_address(self) -> ChecksumAddress: - # Contract calls should always return checksummed addresses - address: ChecksumAddress = self.router.functions.WETH().call() + if self.version == 2: + # Contract calls should always return checksummed addresses + address: ChecksumAddress = self.router.functions.WETH().call() + elif self.version == 3: + address = self.router.functions.WETH9().call() return address def _load_contract(self, abi_name: str, address: AddressLike) -> Contract: @@ -365,94 +385,164 @@ def get_fee_taker(self) -> float: return 0.003 # ------ Market -------------------------------------------------------------------- - @supports([1, 2]) - def get_eth_token_input_price(self, token: AddressLike, qty: Wei) -> Wei: + @supports([1, 2, 3]) + def get_eth_token_input_price( + self, token: AddressLike, qty: Wei, fee: int = 3000 + ) -> Wei: """Public price for ETH to Token trades with an exact input.""" if self.version == 1: ex = self.exchange_contract(token) price: Wei = ex.functions.getEthToTokenInputPrice(qty).call() - else: + 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 + ) return price - @supports([1, 2]) - def get_token_eth_input_price(self, token: AddressLike, qty: int) -> int: + @supports([1, 2, 3]) + def get_token_eth_input_price( + self, token: AddressLike, qty: int, fee: int = 3000 + ) -> int: """Public price for token to ETH trades with an exact input.""" if self.version == 1: 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 + ) return price - @supports([2]) + @supports([2, 3]) def get_token_token_input_price( self, token0: AnyAddress, token1: AnyAddress, qty: int, route: Optional[List[AnyAddress]] = None, + fee: int = 3000, ) -> 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 not route: - 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)) + """ + Public price for token to token trades with an exact input. - route = route or [token0, self.get_weth_address(), token1] - price: int = self.router.functions.getAmountsOut(qty, route).call()[-1] + :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: + if 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, qty)) + elif is_same_address(token1, self.get_weth_address()): + return int(self.get_token_eth_input_price(token0, qty)) + + 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: + # 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() + raise Exception("custom route not yet supported for v3") + + # FIXME: This comment is no longer valid, but some indication is needed if fee is unset in V3 + logger.warning("Assuming 0.3% fee") + + # 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() return price - @supports([1, 2]) - def get_eth_token_output_price(self, token: AddressLike, qty: int) -> Wei: + @supports([1, 2, 3]) + def get_eth_token_output_price( + self, token: AddressLike, qty: int, fee: int = 3000 + ) -> Wei: """Public price for ETH to Token trades with an exact output.""" if self.version == 1: ex = self.exchange_contract(token) price: Wei = ex.functions.getEthToTokenOutputPrice(qty).call() - else: + elif self.version == 2: route = [self.get_weth_address(), token] price = self.router.functions.getAmountsIn(qty, route).call()[0] + elif self.version == 3: + price = self.get_token_token_output_price( + self.get_weth_address(), token, qty, fee=fee + ) return price - @supports([1, 2]) - def get_token_eth_output_price(self, token: AddressLike, qty: Wei) -> int: + @supports([1, 2, 3]) + def get_token_eth_output_price( + self, token: AddressLike, qty: Wei, fee: int = 3000 + ) -> int: """Public price for token to ETH trades with an exact output.""" if self.version == 1: ex = self.exchange_contract(token) price: int = ex.functions.getTokenToEthOutputPrice(qty).call() - else: + elif self.version == 2: price = self.router.functions.getAmountsIn( qty, [token, self.get_weth_address()] ).call()[0] + elif self.version == 3: + price = self.get_token_token_output_price( + token, self.get_weth_address(), qty, fee=fee + ) return price - @supports([2]) + @supports([2, 3]) def get_token_token_output_price( self, token0: AnyAddress, token1: AnyAddress, qty: int, route: Optional[List[AnyAddress]] = None, + fee: int = 3000, ) -> 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 not route: - 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)) + """ + Public price for token to token trades with an exact output. - route = route or [token0, self.get_weth_address(), token1] - price: int = self.router.functions.getAmountsIn(qty, route).call()[0] + :param fee: (v3 only) The pool's fee in hundredths of a bip, i.e. 1e-6 (3000 is 0.3%) + """ + if not route: + if self.version == 2: + # 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)) + + 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: + # 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() + raise Exception("custom route not yet supported for v3") + + logger.warning("Assuming 0.3% fee") + # 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() return price # ------ Wallet balance ------------------------------------------------------------ @@ -536,7 +626,7 @@ def make_trade( 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 + input_token, output_token, qty, recipient ) @check_approval @@ -559,7 +649,7 @@ def make_trade_output( 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 + input_token, output_token, qty, recipient ) def _eth_to_token_swap_input( @@ -580,7 +670,8 @@ def _eth_to_token_swap_input( 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( @@ -596,6 +687,12 @@ def _eth_to_token_swap_input( ), self._get_tx_params(qty), ) + elif self.version == 3: + return self._token_to_token_swap_input( + self.get_weth_address(), output_token, qty, recipient + ) + else: + raise ValueError def _token_to_eth_swap_input( self, input_token: AddressLike, qty: int, recipient: Optional[AddressLike] @@ -615,7 +712,7 @@ def _token_to_eth_swap_input( 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( @@ -631,15 +728,24 @@ def _token_to_eth_swap_input( self._deadline(), ), ) + elif self.version == 3: + return self._token_to_token_swap_input( + input_token, self.get_weth_address(), qty, recipient + ) + else: + raise ValueError def _token_to_token_swap_input( self, input_token: AddressLike, - qty: int, output_token: AddressLike, + qty: int, recipient: Optional[AddressLike], + fee: int = 3000, ) -> HexBytes: """Convert tokens to tokens given an input amount.""" + if recipient is None: + recipient = self.address if self.version == 1: token_funcs = self.exchange_contract(input_token).functions # TODO: This might not be correct @@ -659,9 +765,7 @@ 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: min_tokens_bought = int( (1 - self.max_slippage) * self.get_token_token_input_price(input_token, output_token, qty) @@ -675,6 +779,33 @@ def _token_to_token_swap_input( self._deadline(), ), ) + elif self.version == 3: + min_tokens_bought = int( + (1 - self.max_slippage) + * self.get_token_token_input_price( + input_token, output_token, qty, fee=fee + ) + ) + sqrtPriceLimitX96 = 0 + return self._build_and_send_tx( + self.router.functions.exactInputSingle( + { + "tokenIn": input_token, + "tokenOut": output_token, + "fee": fee, + "recipient": "0x0000000000000000000000000000000000000000", + "deadline": self._deadline(), + "amountIn": qty, + "amountOutMinimum": min_tokens_bought, + "sqrtPriceLimitX96": sqrtPriceLimitX96, + } + ), + self._get_tx_params( + Wei(qty) if input_token == self.get_weth_address() else Wei(0) + ), + ) + else: + raise ValueError def _eth_to_token_swap_output( self, output_token: AddressLike, qty: int, recipient: Optional[AddressLike] @@ -691,7 +822,7 @@ def _eth_to_token_swap_output( 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 = int( @@ -707,6 +838,12 @@ def _eth_to_token_swap_output( ), self._get_tx_params(eth_qty), ) + elif self.version == 3: + return self._token_to_token_swap_output( + self.get_weth_address(), output_token, qty, recipient + ) + else: + raise ValueError def _token_to_eth_swap_output( self, input_token: AddressLike, qty: Wei, recipient: Optional[AddressLike] @@ -740,7 +877,7 @@ def _token_to_eth_swap_output( func_params.append(recipient) function = token_funcs.tokenToEthTransferOutput(*func_params) return self._build_and_send_tx(function) - else: + elif self.version == 2: max_tokens = int((1 + self.max_slippage) * cost) return self._build_and_send_tx( self.router.functions.swapTokensForExactETH( @@ -751,15 +888,26 @@ def _token_to_eth_swap_output( self._deadline(), ), ) + elif self.version == 3: + return self._token_to_token_swap_output( + input_token, self.get_weth_address(), qty, recipient + ) + else: + raise ValueError def _token_to_token_swap_output( self, input_token: AddressLike, - qty: int, output_token: AddressLike, + qty: int, recipient: Optional[AddressLike], + fee: int = 3000, ) -> HexBytes: - """Convert tokens to tokens given an output amount.""" + """ + Convert tokens to tokens given an output amount. + + :param fee: TODO + """ if self.version == 1: token_funcs = self.exchange_contract(input_token).functions max_tokens_sold, max_eth_sold = self._calculate_max_input_token( @@ -779,7 +927,9 @@ 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: + elif self.version == 2: + if recipient is None: + recipient = self.address cost = self.get_token_token_output_price(input_token, output_token, qty) amount_in_max = int((1 + self.max_slippage) * cost) return self._build_and_send_tx( @@ -787,10 +937,41 @@ def _token_to_token_swap_output( qty, amount_in_max, [input_token, self.get_weth_address(), output_token], - self.address, + recipient, self._deadline(), ), ) + elif self.version == 3: + if recipient is None: + recipient = self.address + + cost = self.get_token_token_output_price( + input_token, output_token, qty, fee=fee + ) + amount_in_max = int((1 + self.max_slippage) * cost) + sqrtPriceLimitX96 = 0 + + 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( + Wei(amount_in_max) + if input_token == self.get_weth_address() + else Wei(0) + ), + ) + else: + raise ValueError # ------ Approval Utils ------------------------------------------------------------ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: @@ -804,7 +985,7 @@ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> Non function = self.erc20_contract(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) @@ -816,7 +997,7 @@ def _is_approved(self, token: AddressLike) -> bool: _validate_address(token) if self.version == 1: contract_addr = self.exchange_address_from_token(token) - else: + elif self.version in [2, 3]: contract_addr = self.router_address amount = ( self.erc20_contract(token) @@ -926,26 +1107,7 @@ def _calculate_max_output_token( # ------ Test utilities ------------------------------------------------------------ - def _buy_test_assets(self) -> None: - """ - Buys some BAT and DAI. - Used in testing. - """ - ONE_ETH = 1 * 10 ** 18 - TEST_AMT = int(0.1 * ONE_ETH) - tokens = self._get_token_addresses() - - 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 - ) - self.w3.eth.waitForTransactionReceipt(tx) - - def _get_token_addresses(self) -> Dict[str, str]: + def _get_token_addresses(self) -> Dict[str, ChecksumAddress]: """ Returns a dict with addresses for tokens for the current net. Used in testing. @@ -953,20 +1115,8 @@ def _get_token_addresses(self) -> Dict[str, str]: 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" - ), - } + return tokens elif netname == "rinkeby": - return { - "eth": "0x0000000000000000000000000000000000000000", - "bat": "0xDA5B056Cfb861282B4b59d29c9B395bcC238D29B", - "dai": "0x2448eE2641d78CC42D7AD76498917359D961A783", - } + return tokens_rinkeby else: raise Exception(f"Unknown net '{netname}'")