From dc5ee9cca5d745ec0f1646272305f5bc63f0efd4 Mon Sep 17 00:00:00 2001 From: KeremP Date: Tue, 18 Jan 2022 00:51:38 -0500 Subject: [PATCH 01/32] implementing V3 features. added methods to fetch on-chain pool data --- Makefile | 4 +- tests/test_uniswap.py | 51 + .../uniswap-v3/nonFungiblePositionManager.abi | 1221 +++++++++++++++++ uniswap/uniswap.py | 83 +- 4 files changed, 1356 insertions(+), 3 deletions(-) create mode 100644 uniswap/assets/uniswap-v3/nonFungiblePositionManager.abi diff --git a/Makefile b/Makefile index ac1ebe7..eeb3806 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test typecheck lint precommit docs test: - poetry run pytest -v --tb=line --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml + poetry run pytest -v --tb=native --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml typecheck: poetry run mypy --pretty @@ -11,7 +11,7 @@ lint: format: black uniswap - + format-abis: npx prettier --write --parser=json uniswap/assets/*/*.abi diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 08ea0b7..dec1b85 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -193,6 +193,57 @@ def test_get_price_output(self, client, token0, token1, qty, kwargs): r = client.get_price_output(token0, token1, qty, **kwargs) assert r + @pytest.mark.parametrize( + "token0, token1, kwargs", + [ + (weth, dai, {"fee": 500}), + ] + ) + def test_get_pool_instance(self, client, token0, token1, kwargs): + 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( + "token0, token1, kwargs", + [ + (weth, dai, {"fee": 500}), + ] + ) + def test_get_pool_immutables(self, client, token0, token1, kwargs): + 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) + assert r + + @pytest.mark.parametrize( + "token0, token1, kwargs", + [ + (weth, dai, {"fee": 500}), + ] + ) + def test_get_pool_state(self, client, token0, token1, kwargs): + 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) + assert r + + @pytest.mark.parametrize( + "token0, token1, amount0, amount1, slippage, kwargs", + [ + (weth, dai, 10, 10, .02, {"fee": 500}), + ] + ) + def test_mint_position(self, client, token0, token1, amount0, amount1, slippage, kwargs): + 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, slippage) + assert r + # ------ ERC20 Pool ---------------------------------------------------------------- @pytest.mark.parametrize("token", [(bat), (dai)]) def test_get_ex_eth_balance( diff --git a/uniswap/assets/uniswap-v3/nonFungiblePositionManager.abi b/uniswap/assets/uniswap-v3/nonFungiblePositionManager.abi new file mode 100644 index 0000000..5412fa6 --- /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/uniswap.py b/uniswap/uniswap.py index ccc5639..922e517 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -17,7 +17,7 @@ HexBytes, ) -from .types import AddressLike +from .types import AddressLike, Contract from .token import ERC20Token from .tokens import tokens, tokens_rinkeby from .exceptions import InvalidToken, InsufficientBalance @@ -166,6 +166,10 @@ def __init__( 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 + ) else: raise Exception( f"Invalid version '{self.version}', only 1, 2 or 3 supported" @@ -1243,6 +1247,83 @@ def get_weth_address(self) -> ChecksumAddress: address = self.router.functions.WETH9().call() return address + @supports([3]) + def get_pool_instance( + self, token_in: AddressLike, token_out: AddressLike, fee: int = 3000 + ) -> Contract: + """ + Returns an instance of a pool contract for a given token pair and fee. + Requires pair [token_in, token_out] has a direct pool. + + """ + if token_in == ETH_ADDRESS: + token_in = self.get_weth_address() + if token_out == ETH_ADDRESS: + token_out = self.get_weth_address() + + params: Iterable[Union[ChecksumAddress, int]] = [ + self.w3.toChecksumAddress(token_in), + self.w3.toChecksumAddress(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 + ) + return pool_contract + + @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 + + # TODO: define Position struct + + @supports([3]) + def mint_position( + self, pool: Contract, amount0: int, amount1: int, slippage: float + ) -> None: + + positionManager = self.nonFungiblePositionManager + + + return + @supports([2, 3]) def get_raw_price( self, token_in: AddressLike, token_out: AddressLike, fee: int = 3000 From 1613ea16d5d106aed7e0aa81e3e99f97f3a88d90 Mon Sep 17 00:00:00 2001 From: KeremP Date: Sun, 23 Jan 2022 15:00:31 -0500 Subject: [PATCH 02/32] feature: adding position miniting (WIP) --- Makefile | 2 +- tests/test_uniswap.py | 11 +++++++---- uniswap/uniswap.py | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index eeb3806..63adb60 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test typecheck lint precommit docs test: - poetry run pytest -v --tb=native --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml + poetry run pytest -v -s --full-trace --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml typecheck: poetry run mypy --pretty diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index dec1b85..60c1a7d 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -216,6 +216,7 @@ def test_get_pool_immutables(self, client, token0, token1, kwargs): 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( @@ -229,19 +230,21 @@ def test_get_pool_state(self, client, token0, token1, kwargs): 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( - "token0, token1, amount0, amount1, slippage, kwargs", + "amount0, amount1, token0, token1, kwargs", [ - (weth, dai, 10, 10, .02, {"fee": 500}), + (1000, 1000, weth, dai, {"fee":500}), ] ) - def test_mint_position(self, client, token0, token1, amount0, amount1, slippage, kwargs): + def test_mint_position(self, client, amount0, amount1, token0, token1, kwargs): 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, slippage) + r = client.mint_position(pool, amount0, amount1) + print(r) assert r # ------ ERC20 Pool ---------------------------------------------------------------- diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 922e517..1ce3030 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1316,13 +1316,44 @@ def get_pool_state( @supports([3]) def mint_position( - self, pool: Contract, amount0: int, amount1: int, slippage: float + self, pool: Contract, amount0: int, amount1: int ) -> None: + #TODO: add to constants.py + MIN_TICK = -887272 + MAX_TICK = -MIN_TICK + + pool_sate = self.get_pool_state(pool) + pool_immutables = 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 = self._build_and_send_tx(approve0) + self.w3.eth.wait_for_transaction_receipt(tx0, timeout=6000) - return + approve1 = _load_contract_erc20(self.w3, token1).functions.approve( + self.positionManager_addr, amount1 + ) + 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) + + 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(position) + self.w3.eth.wait_for_transaction_receipt(mint_tx, timeout=6000) + + return mint_tx @supports([2, 3]) def get_raw_price( From a331538b1c73d76c75301cc398294295fccbee4a Mon Sep 17 00:00:00 2001 From: KeremP Date: Sun, 13 Feb 2022 22:35:45 -0500 Subject: [PATCH 03/32] WIP testing V3 pool position minting --- tests/test_uniswap.py | 2 +- uniswap/uniswap.py | 24 +++++++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 60c1a7d..0a34119 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -236,7 +236,7 @@ def test_get_pool_state(self, client, token0, token1, kwargs): @pytest.mark.parametrize( "amount0, amount1, token0, token1, kwargs", [ - (1000, 1000, weth, dai, {"fee":500}), + (1, 100, weth, dai, {"fee":500}), ] ) def test_mint_position(self, client, amount0, amount1, token0, token1, kwargs): diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 1ce3030..1f78749 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1312,7 +1312,7 @@ def get_pool_state( return pool_state - # TODO: define Position struct + # FIXME: mint call reverting - likely to do w/ passing struct args to contract function call @supports([3]) def mint_position( @@ -1346,14 +1346,24 @@ def mint_position( tx1 = self._build_and_send_tx(approve1) self.w3.eth.wait_for_transaction_receipt(tx1, timeout=6000) - position = positionManager.functions.mint({ - 'token0':token0,'token1':token1,'fee':fee,'tickLower':MIN_TICK,'tickUpper':MAX_TICK, + position = positionManager.encodeABI(fn_name="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() - }) - mint_tx = self._build_and_send_tx(position) - self.w3.eth.wait_for_transaction_receipt(mint_tx, timeout=6000) + }]) + print(position) - return mint_tx + multicall = positionManager.functions.multicall([position]).transact({"from":_addr_to_str(self.address), "gas":417918}) + + print(multicall) + # + # 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([2, 3]) def get_raw_price( From 61919013e1343fccea17d451d3372abb7d4d3656 Mon Sep 17 00:00:00 2001 From: KeremP Date: Sun, 24 Apr 2022 15:58:32 -0400 Subject: [PATCH 04/32] wip v3 features --- tests/test_uniswap.py | 2 +- uniswap/uniswap.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 0a34119..9cdabbd 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -236,7 +236,7 @@ def test_get_pool_state(self, client, token0, token1, kwargs): @pytest.mark.parametrize( "amount0, amount1, token0, token1, kwargs", [ - (1, 100, weth, dai, {"fee":500}), + (1, 10, weth, dai, {"fee":500}), ] ) def test_mint_position(self, client, amount0, amount1, token0, token1, kwargs): diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 1f78749..b11c720 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1312,7 +1312,7 @@ def get_pool_state( return pool_state - # 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( @@ -1340,11 +1340,13 @@ def mint_position( self.w3.eth.wait_for_transaction_receipt(tx0, timeout=6000) approve1 = _load_contract_erc20(self.w3, token1).functions.approve( - self.positionManager_addr, amount1 + 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.encodeABI(fn_name="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() @@ -1354,6 +1356,12 @@ def mint_position( multicall = positionManager.functions.multicall([position]).transact({"from":_addr_to_str(self.address), "gas":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) From 7b4aedfad463f60f25c280490a0301201f14bc74 Mon Sep 17 00:00:00 2001 From: KeremP Date: Mon, 11 Jul 2022 00:19:25 -0400 Subject: [PATCH 05/32] feat: create v3 pool and mint/add liquidity position --- Makefile | 2 +- tests/test_uniswap.py | 93 +++++++++------------- uniswap/constants.py | 11 +++ uniswap/uniswap.py | 178 ++++++++++++++++++++++++------------------ uniswap/util.py | 38 +++++++++ 5 files changed, 189 insertions(+), 133 deletions(-) diff --git a/Makefile b/Makefile index 63adb60..592de94 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test typecheck lint precommit docs test: - poetry run pytest -v -s --full-trace --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml + poetry run pytest -v --tb=line --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml typecheck: poetry run mypy --pretty diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index bd37d62..03b9bbc 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -11,10 +11,10 @@ from web3 import Web3 from web3.exceptions import NameNotFound -from uniswap import Uniswap -from uniswap.constants import ETH_ADDRESS +from uniswap import Uniswap, token +from uniswap.constants import ETH_ADDRESS, WETH9_ADDRESS from uniswap.exceptions import InsufficientBalance -from uniswap.util import _str_to_addr +from uniswap.util import _str_to_addr, default_tick_range, _addr_to_str, _load_contract_erc20 logger = logging.getLogger(__name__) @@ -193,60 +193,6 @@ def test_get_price_output(self, client, token0, token1, qty, kwargs): r = client.get_price_output(token0, token1, qty, **kwargs) assert r - @pytest.mark.parametrize( - "token0, token1, kwargs", - [ - (weth, dai, {"fee": 500}), - ] - ) - def test_get_pool_instance(self, client, token0, token1, kwargs): - 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( - "token0, token1, kwargs", - [ - (weth, dai, {"fee": 500}), - ] - ) - def test_get_pool_immutables(self, client, token0, token1, kwargs): - 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, kwargs", - [ - (weth, dai, {"fee": 500}), - ] - ) - def test_get_pool_state(self, client, token0, token1, kwargs): - 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":500}), - ] - ) - def test_mint_position(self, client, amount0, amount1, token0, token1, kwargs): - 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)]) def test_get_ex_eth_balance( @@ -280,6 +226,39 @@ def get_exchange_rate( assert r # ------ Liquidity ----------------------------------------------------------------- + @pytest.mark.parametrize( + "token0, token1, amount0, amount1, qty, fee", + [ + (dai, usdc, ONE_ETH, ONE_USDC, ONE_ETH, 3000), + ] + ) + def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, 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(token0, token1, fee) + except Exception: + pool = client.get_pool_instance(token0, token1, fee) + + # Ensuring client has sufficient balance of both tokens + eth_to_dai = client.make_trade(ETH_ADDRESS, token0, qty, None) + eth_to_dai_tx = client.w3.eth.wait_for_transaction_receipt(eth_to_dai, timeout=RECEIPT_TIMEOUT) + + dai_to_usdc = client.make_trade(token0, token1, amount1, None) + dai_to_usdc_tx = client.w3.eth.wait_for_transaction_receipt(dai_to_usdc, timeout=RECEIPT_TIMEOUT) + + 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 == 1 + @pytest.mark.skip @pytest.mark.parametrize( "token, max_eth", diff --git a/uniswap/constants.py b/uniswap/constants.py index 3a51fad..b14c93c 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -56,3 +56,14 @@ "harmony_mainnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", "harmony_testnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", } + +# 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 = { + 500: 10, + 3_000: 60, + 10_000: 200 +} \ No newline at end of file diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index b11c720..3424889 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1,3 +1,4 @@ +from multiprocessing import pool import os import time import logging @@ -10,6 +11,7 @@ from web3.exceptions import BadFunctionCallOutput, ContractLogicError from web3.types import ( TxParams, + TxReceipt, Wei, Address, ChecksumAddress, @@ -27,7 +29,9 @@ _validate_address, _load_contract, _load_contract_erc20, + encode_sqrt_ratioX96, is_same_address, + nearest_tick, ) from .decorators import supports, check_approval from .constants import ( @@ -35,6 +39,7 @@ _factory_contract_addresses_v1, _factory_contract_addresses_v2, _router_contract_addresses_v2, + _tick_spacing, ETH_ADDRESS, ) @@ -427,6 +432,7 @@ def make_trade( input_token, qty, recipient, fee, slippage, fee_on_transfer ) else: + print(input_token) return self._token_to_token_swap_input( input_token, output_token, @@ -1037,6 +1043,7 @@ def get_exchange_rate(self, token: AddressLike) -> float: return float(token_reserve / eth_reserve) # ------ Liquidity ----------------------------------------------------------------- + # TODO: add v3 @supports([1]) @check_approval def add_liquidity( @@ -1249,29 +1256,111 @@ def get_weth_address(self) -> ChecksumAddress: @supports([3]) def get_pool_instance( - self, token_in: AddressLike, token_out: AddressLike, fee: int = 3000 + 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] has a direct pool. + Requires pair [token_in, token_out, fee] has a direct pool. """ - if token_in == ETH_ADDRESS: - token_in = self.get_weth_address() - if token_out == ETH_ADDRESS: - token_out = self.get_weth_address() - params: Iterable[Union[ChecksumAddress, int]] = [ - self.w3.toChecksumAddress(token_in), - self.w3.toChecksumAddress(token_out), - fee, - ] - pool_address = self.factory_contract.functions.getPool(*params).call() - pool_contract = _load_contract( + assert token_0 != token_1, "Token addresses cannot be the same" + assert fee in list(_tick_spacing.keys()), "Uniswap V3 only supports three levels of fees: 0.05%, 0.3%, 1%" + + pool_address = self.factory_contract.functions.getPool(token_0, token_1, fee).call() + pool_instance = _load_contract( self.w3, abi_name="uniswap-v3/pool", address=pool_address ) - return pool_contract + + 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" + assert fee in list(_tick_spacing.keys()), "Uniswap V3 only supports three levels of fees: 0.05%, 0.3%, 1%" + + 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().processReceipt(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 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 + """ + address = _addr_to_str(self.address) + 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 + ) + + assert token_0_instance.functions.balanceOf(address).call() > amount_0 + assert token_1_instance.functions.balanceOf(address).call() > amount_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':address}) + + nft_manager = self.nonFungiblePositionManager + token_0_instance.functions.approve(nft_manager.address, amount_0).transact({'from':address}) + token_1_instance.functions.approve(nft_manager.address, amount_1).transact({'from':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':address}) + receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) + + return receipt + + + @supports([3]) def get_pool_immutables( self, pool: Contract @@ -1312,67 +1401,6 @@ def get_pool_state( return pool_state - # FIXME: mint call reverting - likely due to handling of token amounts - - @supports([3]) - def mint_position( - self, pool: Contract, amount0: int, amount1: int - ) -> None: - - #TODO: add to constants.py - MIN_TICK = -887272 - MAX_TICK = -MIN_TICK - - pool_sate = self.get_pool_state(pool) - pool_immutables = 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 = 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.encodeABI(fn_name="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":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([2, 3]) def get_raw_price( self, token_in: AddressLike, token_out: AddressLike, fee: int = 3000 diff --git a/uniswap/util.py b/uniswap/util.py index 1c7b710..9b856d1 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -1,11 +1,13 @@ import os import json +import math import functools from typing import Union, List, Tuple from web3 import Web3 from web3.exceptions import NameNotFound +from .constants import MIN_TICK, MAX_TICK, _tick_spacing from .types import AddressLike, Address, Contract @@ -64,3 +66,39 @@ def _encode_path(token_in: AddressLike, route: List[Tuple[int, AddressLike]]) -> 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)) + +# 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 From c736d49a5290bf91708d4c53d665c21bcfb23263 Mon Sep 17 00:00:00 2001 From: KeremP Date: Mon, 11 Jul 2022 00:24:26 -0400 Subject: [PATCH 06/32] fix: add 0x0 address assert in get_pool_instance --- uniswap/uniswap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 3424889..86919f2 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1261,6 +1261,7 @@ def get_pool_instance( """ 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. """ @@ -1268,6 +1269,7 @@ def get_pool_instance( assert fee in list(_tick_spacing.keys()), "Uniswap V3 only supports three levels of fees: 0.05%, 0.3%, 1%" 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 ) From 99180a9c2d46cda644e7498f8f938282a887e0c4 Mon Sep 17 00:00:00 2001 From: KeremP Date: Tue, 12 Jul 2022 23:20:55 -0400 Subject: [PATCH 07/32] feat: close v3 liquidity position --- tests/test_uniswap.py | 21 ++++++ uniswap/constants.py | 2 + uniswap/uniswap.py | 171 ++++++++++++++++++++++++++---------------- 3 files changed, 129 insertions(+), 65 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 03b9bbc..1d3ea02 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -1,3 +1,4 @@ +from async_timeout import timeout import pytest import os import subprocess @@ -258,6 +259,26 @@ def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, token0, token1, am deadline=2**64 ) assert r.status == 1 + + 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 == 1 + @pytest.mark.skip @pytest.mark.parametrize( diff --git a/uniswap/constants.py b/uniswap/constants.py index b14c93c..1e60346 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -57,6 +57,8 @@ "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 diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 86919f2..d1c9926 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -35,6 +35,8 @@ ) from .decorators import supports, check_approval from .constants import ( + MAX_UINT_128, + WETH9_ADDRESS, _netid_to_name, _factory_contract_addresses_v1, _factory_contract_addresses_v2, @@ -1043,7 +1045,6 @@ def get_exchange_rate(self, token: AddressLike) -> float: return float(token_reserve / eth_reserve) # ------ Liquidity ----------------------------------------------------------------- - # TODO: add v3 @supports([1]) @check_approval def add_liquidity( @@ -1068,6 +1069,96 @@ def remove_liquidity(self, token: str, max_token: int) -> HexBytes: ) 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 + """ + address = _addr_to_str(self.address) + 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 + ) + + assert token_0_instance.functions.balanceOf(address).call() > amount_0 + assert token_1_instance.functions.balanceOf(address).call() > amount_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':address}) + + nft_manager = self.nonFungiblePositionManager + token_0_instance.functions.approve(nft_manager.address, amount_0).transact({'from':address}) + token_1_instance.functions.approve(nft_manager.address, amount_1).transact({'from':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':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) -> TxReceipt: + position = self.nonFungiblePositionManager.functions.positions(tokenId).call() + + if deadline is None: + deadline = self._deadline() + + 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 + + # ------ Approval Utils ------------------------------------------------------------ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: """Give an exchange/router max approval of a token.""" @@ -1299,70 +1390,6 @@ def create_pool_instance( return pool_instance - @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 - """ - address = _addr_to_str(self.address) - 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 - ) - - assert token_0_instance.functions.balanceOf(address).call() > amount_0 - assert token_1_instance.functions.balanceOf(address).call() > amount_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':address}) - - nft_manager = self.nonFungiblePositionManager - token_0_instance.functions.approve(nft_manager.address, amount_0).transact({'from':address}) - token_1_instance.functions.approve(nft_manager.address, amount_1).transact({'from':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':address}) - receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) - - return receipt - - - @supports([3]) def get_pool_immutables( self, pool: Contract @@ -1402,6 +1429,20 @@ def get_pool_state( } 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 @supports([2, 3]) def get_raw_price( From 760b50f9c2029ab63c5944de5bf2e289af6fa1ac Mon Sep 17 00:00:00 2001 From: KeremP Date: Wed, 13 Jul 2022 00:06:26 -0400 Subject: [PATCH 08/32] minor cleanups --- tests/test_uniswap.py | 2 -- uniswap/uniswap.py | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 1d3ea02..63cbd31 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -266,7 +266,6 @@ def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, token0, token1, am position_array = client.get_liquidity_positions() assert len(position_array) > 0 - @pytest.mark.parametrize( "deadline", [(2**64)], @@ -279,7 +278,6 @@ def test_close_position(self, client: Uniswap, deadline): r = client.close_position(tokenId, deadline=deadline) assert r.status == 1 - @pytest.mark.skip @pytest.mark.parametrize( "token, max_eth", diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index d1c9926..7cdce1d 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1132,12 +1132,23 @@ def mint_liquidity( # TODO: should this be multiple functions? @supports([3]) - def close_position(self, tokenId: int, amount0Min: int = 0, amount1Min: int = 0, deadline: int = None) -> TxReceipt: + def close_position( + self, + tokenId: int, + amount0Min: int = 0, + amount1Min: int = 0, + deadline: int = 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 @@ -1158,7 +1169,6 @@ def close_position(self, tokenId: int, amount0Min: int = 0, amount1Min: int = 0, return receipt - # ------ Approval Utils ------------------------------------------------------------ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: """Give an exchange/router max approval of a token.""" From 99d1217776d0923213a1fd5646b470bf064a3ed6 Mon Sep 17 00:00:00 2001 From: KeremP Date: Thu, 14 Jul 2022 20:16:56 -0400 Subject: [PATCH 09/32] add get_tvl_in_pool to return total value locked in pool --- uniswap/uniswap.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 7cdce1d..72407dc 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -35,7 +35,9 @@ ) from .decorators import supports, check_approval from .constants import ( + MAX_TICK, MAX_UINT_128, + MIN_TICK, WETH9_ADDRESS, _netid_to_name, _factory_contract_addresses_v1, @@ -1169,6 +1171,33 @@ def close_position( return receipt + def get_token0_in_pool(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(liquidity: float, sqrtPrice: float, sqrtPriceLow: float, sqrtPriceHigh: float) -> float: + sqrtPrice = max(min(sqrtPrice, sqrtPriceHigh), sqrtPriceLow) + return liquidity * (sqrtPrice - sqrtPriceLow) + + def get_tvl_in_pool(self, pool: Contract) -> Tuple[float,float]: + 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 + token1_liquidity = 0 + liquidity_total = 0 + TICK_SPACING = _tick_spacing[fee] + for tick in range(MIN_TICK, MAX_TICK, TICK_SPACING): + tick_liquidity = pool.functions.ticks(tick).call() + liquidity_total += tick_liquidity.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) + return (token0_liquidity, token1_liquidity) + # ------ Approval Utils ------------------------------------------------------------ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: """Give an exchange/router max approval of a token.""" From c95e1195d46d6c98261f7b7a977141944be6976b Mon Sep 17 00:00:00 2001 From: KeremP Date: Tue, 18 Jan 2022 00:51:38 -0500 Subject: [PATCH 10/32] implementing V3 features. added methods to fetch on-chain pool data --- Makefile | 4 +- tests/test_uniswap.py | 51 + .../uniswap-v3/nonFungiblePositionManager.abi | 1221 +++++++++++++++++ uniswap/uniswap.py | 83 +- 4 files changed, 1356 insertions(+), 3 deletions(-) create mode 100644 uniswap/assets/uniswap-v3/nonFungiblePositionManager.abi diff --git a/Makefile b/Makefile index ac1ebe7..eeb3806 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test typecheck lint precommit docs test: - poetry run pytest -v --tb=line --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml + poetry run pytest -v --tb=native --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml typecheck: poetry run mypy --pretty @@ -11,7 +11,7 @@ lint: format: black uniswap - + format-abis: npx prettier --write --parser=json uniswap/assets/*/*.abi diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index c8511a1..20310a4 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -193,6 +193,57 @@ def test_get_price_output(self, client, token0, token1, qty, kwargs): r = client.get_price_output(token0, token1, qty, **kwargs) assert r + @pytest.mark.parametrize( + "token0, token1, kwargs", + [ + (weth, dai, {"fee": 500}), + ] + ) + def test_get_pool_instance(self, client, token0, token1, kwargs): + 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( + "token0, token1, kwargs", + [ + (weth, dai, {"fee": 500}), + ] + ) + def test_get_pool_immutables(self, client, token0, token1, kwargs): + 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) + assert r + + @pytest.mark.parametrize( + "token0, token1, kwargs", + [ + (weth, dai, {"fee": 500}), + ] + ) + def test_get_pool_state(self, client, token0, token1, kwargs): + 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) + assert r + + @pytest.mark.parametrize( + "token0, token1, amount0, amount1, slippage, kwargs", + [ + (weth, dai, 10, 10, .02, {"fee": 500}), + ] + ) + def test_mint_position(self, client, token0, token1, amount0, amount1, slippage, kwargs): + 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, slippage) + assert r + # ------ ERC20 Pool ---------------------------------------------------------------- @pytest.mark.parametrize("token", [(bat), (dai)]) def test_get_ex_eth_balance( diff --git a/uniswap/assets/uniswap-v3/nonFungiblePositionManager.abi b/uniswap/assets/uniswap-v3/nonFungiblePositionManager.abi new file mode 100644 index 0000000..5412fa6 --- /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/uniswap.py b/uniswap/uniswap.py index ccc5639..922e517 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -17,7 +17,7 @@ HexBytes, ) -from .types import AddressLike +from .types import AddressLike, Contract from .token import ERC20Token from .tokens import tokens, tokens_rinkeby from .exceptions import InvalidToken, InsufficientBalance @@ -166,6 +166,10 @@ def __init__( 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 + ) else: raise Exception( f"Invalid version '{self.version}', only 1, 2 or 3 supported" @@ -1243,6 +1247,83 @@ def get_weth_address(self) -> ChecksumAddress: address = self.router.functions.WETH9().call() return address + @supports([3]) + def get_pool_instance( + self, token_in: AddressLike, token_out: AddressLike, fee: int = 3000 + ) -> Contract: + """ + Returns an instance of a pool contract for a given token pair and fee. + Requires pair [token_in, token_out] has a direct pool. + + """ + if token_in == ETH_ADDRESS: + token_in = self.get_weth_address() + if token_out == ETH_ADDRESS: + token_out = self.get_weth_address() + + params: Iterable[Union[ChecksumAddress, int]] = [ + self.w3.toChecksumAddress(token_in), + self.w3.toChecksumAddress(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 + ) + return pool_contract + + @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 + + # TODO: define Position struct + + @supports([3]) + def mint_position( + self, pool: Contract, amount0: int, amount1: int, slippage: float + ) -> None: + + positionManager = self.nonFungiblePositionManager + + + return + @supports([2, 3]) def get_raw_price( self, token_in: AddressLike, token_out: AddressLike, fee: int = 3000 From 62c80f234365c66aa9ea5d00e85a4c6586a158dc Mon Sep 17 00:00:00 2001 From: KeremP Date: Sun, 23 Jan 2022 15:00:31 -0500 Subject: [PATCH 11/32] feature: adding position miniting (WIP) --- Makefile | 2 +- tests/test_uniswap.py | 11 +++++++---- uniswap/uniswap.py | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index eeb3806..63adb60 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test typecheck lint precommit docs test: - poetry run pytest -v --tb=native --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml + poetry run pytest -v -s --full-trace --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml typecheck: poetry run mypy --pretty diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 20310a4..299c93b 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -216,6 +216,7 @@ def test_get_pool_immutables(self, client, token0, token1, kwargs): 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( @@ -229,19 +230,21 @@ def test_get_pool_state(self, client, token0, token1, kwargs): 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( - "token0, token1, amount0, amount1, slippage, kwargs", + "amount0, amount1, token0, token1, kwargs", [ - (weth, dai, 10, 10, .02, {"fee": 500}), + (1000, 1000, weth, dai, {"fee":500}), ] ) - def test_mint_position(self, client, token0, token1, amount0, amount1, slippage, kwargs): + def test_mint_position(self, client, amount0, amount1, token0, token1, kwargs): 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, slippage) + r = client.mint_position(pool, amount0, amount1) + print(r) assert r # ------ ERC20 Pool ---------------------------------------------------------------- diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 922e517..1ce3030 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1316,13 +1316,44 @@ def get_pool_state( @supports([3]) def mint_position( - self, pool: Contract, amount0: int, amount1: int, slippage: float + self, pool: Contract, amount0: int, amount1: int ) -> None: + #TODO: add to constants.py + MIN_TICK = -887272 + MAX_TICK = -MIN_TICK + + pool_sate = self.get_pool_state(pool) + pool_immutables = 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 = self._build_and_send_tx(approve0) + self.w3.eth.wait_for_transaction_receipt(tx0, timeout=6000) - return + approve1 = _load_contract_erc20(self.w3, token1).functions.approve( + self.positionManager_addr, amount1 + ) + 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) + + 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(position) + self.w3.eth.wait_for_transaction_receipt(mint_tx, timeout=6000) + + return mint_tx @supports([2, 3]) def get_raw_price( From b32acc82c00d46929237d14ba6141bf1e525cbcb Mon Sep 17 00:00:00 2001 From: KeremP Date: Sun, 13 Feb 2022 22:35:45 -0500 Subject: [PATCH 12/32] WIP testing V3 pool position minting --- tests/test_uniswap.py | 2 +- uniswap/uniswap.py | 24 +++++++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 299c93b..ce50d00 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -236,7 +236,7 @@ def test_get_pool_state(self, client, token0, token1, kwargs): @pytest.mark.parametrize( "amount0, amount1, token0, token1, kwargs", [ - (1000, 1000, weth, dai, {"fee":500}), + (1, 100, weth, dai, {"fee":500}), ] ) def test_mint_position(self, client, amount0, amount1, token0, token1, kwargs): diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 1ce3030..1f78749 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1312,7 +1312,7 @@ def get_pool_state( return pool_state - # TODO: define Position struct + # FIXME: mint call reverting - likely to do w/ passing struct args to contract function call @supports([3]) def mint_position( @@ -1346,14 +1346,24 @@ def mint_position( tx1 = self._build_and_send_tx(approve1) self.w3.eth.wait_for_transaction_receipt(tx1, timeout=6000) - position = positionManager.functions.mint({ - 'token0':token0,'token1':token1,'fee':fee,'tickLower':MIN_TICK,'tickUpper':MAX_TICK, + position = positionManager.encodeABI(fn_name="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() - }) - mint_tx = self._build_and_send_tx(position) - self.w3.eth.wait_for_transaction_receipt(mint_tx, timeout=6000) + }]) + print(position) - return mint_tx + multicall = positionManager.functions.multicall([position]).transact({"from":_addr_to_str(self.address), "gas":417918}) + + print(multicall) + # + # 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([2, 3]) def get_raw_price( From 8cf2664c587af5dec1ff0eadc97e8df9741fd2b4 Mon Sep 17 00:00:00 2001 From: KeremP Date: Sun, 24 Apr 2022 15:58:32 -0400 Subject: [PATCH 13/32] wip v3 features --- tests/test_uniswap.py | 2 +- uniswap/uniswap.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index ce50d00..bd37d62 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -236,7 +236,7 @@ def test_get_pool_state(self, client, token0, token1, kwargs): @pytest.mark.parametrize( "amount0, amount1, token0, token1, kwargs", [ - (1, 100, weth, dai, {"fee":500}), + (1, 10, weth, dai, {"fee":500}), ] ) def test_mint_position(self, client, amount0, amount1, token0, token1, kwargs): diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 1f78749..b11c720 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1312,7 +1312,7 @@ def get_pool_state( return pool_state - # 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( @@ -1340,11 +1340,13 @@ def mint_position( self.w3.eth.wait_for_transaction_receipt(tx0, timeout=6000) approve1 = _load_contract_erc20(self.w3, token1).functions.approve( - self.positionManager_addr, amount1 + 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.encodeABI(fn_name="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() @@ -1354,6 +1356,12 @@ def mint_position( multicall = positionManager.functions.multicall([position]).transact({"from":_addr_to_str(self.address), "gas":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) From 99a16db1a2d14ef211aa41e4fab8fb710b1e9430 Mon Sep 17 00:00:00 2001 From: KeremP Date: Mon, 11 Jul 2022 00:19:25 -0400 Subject: [PATCH 14/32] feat: create v3 pool and mint/add liquidity position --- Makefile | 2 +- tests/test_uniswap.py | 93 +++++++++------------- uniswap/constants.py | 11 +++ uniswap/uniswap.py | 178 ++++++++++++++++++++++++------------------ uniswap/util.py | 38 +++++++++ 5 files changed, 189 insertions(+), 133 deletions(-) diff --git a/Makefile b/Makefile index 63adb60..592de94 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test typecheck lint precommit docs test: - poetry run pytest -v -s --full-trace --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml + poetry run pytest -v --tb=line --maxfail=4 --cov=uniswap --cov-report html --cov-report term --cov-report xml typecheck: poetry run mypy --pretty diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index bd37d62..03b9bbc 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -11,10 +11,10 @@ from web3 import Web3 from web3.exceptions import NameNotFound -from uniswap import Uniswap -from uniswap.constants import ETH_ADDRESS +from uniswap import Uniswap, token +from uniswap.constants import ETH_ADDRESS, WETH9_ADDRESS from uniswap.exceptions import InsufficientBalance -from uniswap.util import _str_to_addr +from uniswap.util import _str_to_addr, default_tick_range, _addr_to_str, _load_contract_erc20 logger = logging.getLogger(__name__) @@ -193,60 +193,6 @@ def test_get_price_output(self, client, token0, token1, qty, kwargs): r = client.get_price_output(token0, token1, qty, **kwargs) assert r - @pytest.mark.parametrize( - "token0, token1, kwargs", - [ - (weth, dai, {"fee": 500}), - ] - ) - def test_get_pool_instance(self, client, token0, token1, kwargs): - 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( - "token0, token1, kwargs", - [ - (weth, dai, {"fee": 500}), - ] - ) - def test_get_pool_immutables(self, client, token0, token1, kwargs): - 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, kwargs", - [ - (weth, dai, {"fee": 500}), - ] - ) - def test_get_pool_state(self, client, token0, token1, kwargs): - 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":500}), - ] - ) - def test_mint_position(self, client, amount0, amount1, token0, token1, kwargs): - 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)]) def test_get_ex_eth_balance( @@ -280,6 +226,39 @@ def get_exchange_rate( assert r # ------ Liquidity ----------------------------------------------------------------- + @pytest.mark.parametrize( + "token0, token1, amount0, amount1, qty, fee", + [ + (dai, usdc, ONE_ETH, ONE_USDC, ONE_ETH, 3000), + ] + ) + def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, 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(token0, token1, fee) + except Exception: + pool = client.get_pool_instance(token0, token1, fee) + + # Ensuring client has sufficient balance of both tokens + eth_to_dai = client.make_trade(ETH_ADDRESS, token0, qty, None) + eth_to_dai_tx = client.w3.eth.wait_for_transaction_receipt(eth_to_dai, timeout=RECEIPT_TIMEOUT) + + dai_to_usdc = client.make_trade(token0, token1, amount1, None) + dai_to_usdc_tx = client.w3.eth.wait_for_transaction_receipt(dai_to_usdc, timeout=RECEIPT_TIMEOUT) + + 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 == 1 + @pytest.mark.skip @pytest.mark.parametrize( "token, max_eth", diff --git a/uniswap/constants.py b/uniswap/constants.py index 3a51fad..b14c93c 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -56,3 +56,14 @@ "harmony_mainnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", "harmony_testnet": "0x1b02dA8Cb0d097eB8D57A175b88c7D8b47997506", } + +# 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 = { + 500: 10, + 3_000: 60, + 10_000: 200 +} \ No newline at end of file diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index b11c720..3424889 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1,3 +1,4 @@ +from multiprocessing import pool import os import time import logging @@ -10,6 +11,7 @@ from web3.exceptions import BadFunctionCallOutput, ContractLogicError from web3.types import ( TxParams, + TxReceipt, Wei, Address, ChecksumAddress, @@ -27,7 +29,9 @@ _validate_address, _load_contract, _load_contract_erc20, + encode_sqrt_ratioX96, is_same_address, + nearest_tick, ) from .decorators import supports, check_approval from .constants import ( @@ -35,6 +39,7 @@ _factory_contract_addresses_v1, _factory_contract_addresses_v2, _router_contract_addresses_v2, + _tick_spacing, ETH_ADDRESS, ) @@ -427,6 +432,7 @@ def make_trade( input_token, qty, recipient, fee, slippage, fee_on_transfer ) else: + print(input_token) return self._token_to_token_swap_input( input_token, output_token, @@ -1037,6 +1043,7 @@ def get_exchange_rate(self, token: AddressLike) -> float: return float(token_reserve / eth_reserve) # ------ Liquidity ----------------------------------------------------------------- + # TODO: add v3 @supports([1]) @check_approval def add_liquidity( @@ -1249,29 +1256,111 @@ def get_weth_address(self) -> ChecksumAddress: @supports([3]) def get_pool_instance( - self, token_in: AddressLike, token_out: AddressLike, fee: int = 3000 + 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] has a direct pool. + Requires pair [token_in, token_out, fee] has a direct pool. """ - if token_in == ETH_ADDRESS: - token_in = self.get_weth_address() - if token_out == ETH_ADDRESS: - token_out = self.get_weth_address() - params: Iterable[Union[ChecksumAddress, int]] = [ - self.w3.toChecksumAddress(token_in), - self.w3.toChecksumAddress(token_out), - fee, - ] - pool_address = self.factory_contract.functions.getPool(*params).call() - pool_contract = _load_contract( + assert token_0 != token_1, "Token addresses cannot be the same" + assert fee in list(_tick_spacing.keys()), "Uniswap V3 only supports three levels of fees: 0.05%, 0.3%, 1%" + + pool_address = self.factory_contract.functions.getPool(token_0, token_1, fee).call() + pool_instance = _load_contract( self.w3, abi_name="uniswap-v3/pool", address=pool_address ) - return pool_contract + + 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" + assert fee in list(_tick_spacing.keys()), "Uniswap V3 only supports three levels of fees: 0.05%, 0.3%, 1%" + + 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().processReceipt(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 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 + """ + address = _addr_to_str(self.address) + 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 + ) + + assert token_0_instance.functions.balanceOf(address).call() > amount_0 + assert token_1_instance.functions.balanceOf(address).call() > amount_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':address}) + + nft_manager = self.nonFungiblePositionManager + token_0_instance.functions.approve(nft_manager.address, amount_0).transact({'from':address}) + token_1_instance.functions.approve(nft_manager.address, amount_1).transact({'from':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':address}) + receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) + + return receipt + + + @supports([3]) def get_pool_immutables( self, pool: Contract @@ -1312,67 +1401,6 @@ def get_pool_state( return pool_state - # FIXME: mint call reverting - likely due to handling of token amounts - - @supports([3]) - def mint_position( - self, pool: Contract, amount0: int, amount1: int - ) -> None: - - #TODO: add to constants.py - MIN_TICK = -887272 - MAX_TICK = -MIN_TICK - - pool_sate = self.get_pool_state(pool) - pool_immutables = 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 = 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.encodeABI(fn_name="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":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([2, 3]) def get_raw_price( self, token_in: AddressLike, token_out: AddressLike, fee: int = 3000 diff --git a/uniswap/util.py b/uniswap/util.py index 1c7b710..9b856d1 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -1,11 +1,13 @@ import os import json +import math import functools from typing import Union, List, Tuple from web3 import Web3 from web3.exceptions import NameNotFound +from .constants import MIN_TICK, MAX_TICK, _tick_spacing from .types import AddressLike, Address, Contract @@ -64,3 +66,39 @@ def _encode_path(token_in: AddressLike, route: List[Tuple[int, AddressLike]]) -> 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)) + +# 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 From 54e460ee988c8bc9ec53439860eb92ef838087d8 Mon Sep 17 00:00:00 2001 From: KeremP Date: Mon, 11 Jul 2022 00:24:26 -0400 Subject: [PATCH 15/32] fix: add 0x0 address assert in get_pool_instance --- uniswap/uniswap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 3424889..86919f2 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1261,6 +1261,7 @@ def get_pool_instance( """ 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. """ @@ -1268,6 +1269,7 @@ def get_pool_instance( assert fee in list(_tick_spacing.keys()), "Uniswap V3 only supports three levels of fees: 0.05%, 0.3%, 1%" 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 ) From 4151b5caf359d10427280077660a7fc58d60f0d0 Mon Sep 17 00:00:00 2001 From: KeremP Date: Tue, 12 Jul 2022 23:20:55 -0400 Subject: [PATCH 16/32] feat: close v3 liquidity position --- tests/test_uniswap.py | 21 ++++++ uniswap/constants.py | 2 + uniswap/uniswap.py | 171 ++++++++++++++++++++++++++---------------- 3 files changed, 129 insertions(+), 65 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 03b9bbc..1d3ea02 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -1,3 +1,4 @@ +from async_timeout import timeout import pytest import os import subprocess @@ -258,6 +259,26 @@ def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, token0, token1, am deadline=2**64 ) assert r.status == 1 + + 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 == 1 + @pytest.mark.skip @pytest.mark.parametrize( diff --git a/uniswap/constants.py b/uniswap/constants.py index b14c93c..1e60346 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -57,6 +57,8 @@ "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 diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 86919f2..d1c9926 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -35,6 +35,8 @@ ) from .decorators import supports, check_approval from .constants import ( + MAX_UINT_128, + WETH9_ADDRESS, _netid_to_name, _factory_contract_addresses_v1, _factory_contract_addresses_v2, @@ -1043,7 +1045,6 @@ def get_exchange_rate(self, token: AddressLike) -> float: return float(token_reserve / eth_reserve) # ------ Liquidity ----------------------------------------------------------------- - # TODO: add v3 @supports([1]) @check_approval def add_liquidity( @@ -1068,6 +1069,96 @@ def remove_liquidity(self, token: str, max_token: int) -> HexBytes: ) 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 + """ + address = _addr_to_str(self.address) + 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 + ) + + assert token_0_instance.functions.balanceOf(address).call() > amount_0 + assert token_1_instance.functions.balanceOf(address).call() > amount_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':address}) + + nft_manager = self.nonFungiblePositionManager + token_0_instance.functions.approve(nft_manager.address, amount_0).transact({'from':address}) + token_1_instance.functions.approve(nft_manager.address, amount_1).transact({'from':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':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) -> TxReceipt: + position = self.nonFungiblePositionManager.functions.positions(tokenId).call() + + if deadline is None: + deadline = self._deadline() + + 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 + + # ------ Approval Utils ------------------------------------------------------------ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: """Give an exchange/router max approval of a token.""" @@ -1299,70 +1390,6 @@ def create_pool_instance( return pool_instance - @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 - """ - address = _addr_to_str(self.address) - 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 - ) - - assert token_0_instance.functions.balanceOf(address).call() > amount_0 - assert token_1_instance.functions.balanceOf(address).call() > amount_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':address}) - - nft_manager = self.nonFungiblePositionManager - token_0_instance.functions.approve(nft_manager.address, amount_0).transact({'from':address}) - token_1_instance.functions.approve(nft_manager.address, amount_1).transact({'from':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':address}) - receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) - - return receipt - - - @supports([3]) def get_pool_immutables( self, pool: Contract @@ -1402,6 +1429,20 @@ def get_pool_state( } 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 @supports([2, 3]) def get_raw_price( From 6d66b28746ab60fc68ef9257208bb3608ecbde26 Mon Sep 17 00:00:00 2001 From: KeremP Date: Wed, 13 Jul 2022 00:06:26 -0400 Subject: [PATCH 17/32] minor cleanups --- tests/test_uniswap.py | 2 -- uniswap/uniswap.py | 14 ++++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 1d3ea02..63cbd31 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -266,7 +266,6 @@ def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, token0, token1, am position_array = client.get_liquidity_positions() assert len(position_array) > 0 - @pytest.mark.parametrize( "deadline", [(2**64)], @@ -279,7 +278,6 @@ def test_close_position(self, client: Uniswap, deadline): r = client.close_position(tokenId, deadline=deadline) assert r.status == 1 - @pytest.mark.skip @pytest.mark.parametrize( "token, max_eth", diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index d1c9926..7cdce1d 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1132,12 +1132,23 @@ def mint_liquidity( # TODO: should this be multiple functions? @supports([3]) - def close_position(self, tokenId: int, amount0Min: int = 0, amount1Min: int = 0, deadline: int = None) -> TxReceipt: + def close_position( + self, + tokenId: int, + amount0Min: int = 0, + amount1Min: int = 0, + deadline: int = 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 @@ -1158,7 +1169,6 @@ def close_position(self, tokenId: int, amount0Min: int = 0, amount1Min: int = 0, return receipt - # ------ Approval Utils ------------------------------------------------------------ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: """Give an exchange/router max approval of a token.""" From 91b1dc0ba81dea901e301a58be4417d85234012c Mon Sep 17 00:00:00 2001 From: KeremP Date: Thu, 14 Jul 2022 20:16:56 -0400 Subject: [PATCH 18/32] add get_tvl_in_pool to return total value locked in pool --- uniswap/uniswap.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 7cdce1d..72407dc 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -35,7 +35,9 @@ ) from .decorators import supports, check_approval from .constants import ( + MAX_TICK, MAX_UINT_128, + MIN_TICK, WETH9_ADDRESS, _netid_to_name, _factory_contract_addresses_v1, @@ -1169,6 +1171,33 @@ def close_position( return receipt + def get_token0_in_pool(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(liquidity: float, sqrtPrice: float, sqrtPriceLow: float, sqrtPriceHigh: float) -> float: + sqrtPrice = max(min(sqrtPrice, sqrtPriceHigh), sqrtPriceLow) + return liquidity * (sqrtPrice - sqrtPriceLow) + + def get_tvl_in_pool(self, pool: Contract) -> Tuple[float,float]: + 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 + token1_liquidity = 0 + liquidity_total = 0 + TICK_SPACING = _tick_spacing[fee] + for tick in range(MIN_TICK, MAX_TICK, TICK_SPACING): + tick_liquidity = pool.functions.ticks(tick).call() + liquidity_total += tick_liquidity.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) + return (token0_liquidity, token1_liquidity) + # ------ Approval Utils ------------------------------------------------------------ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: """Give an exchange/router max approval of a token.""" From b0fca2be171ad974f1846a9ef7b870168a9d09d8 Mon Sep 17 00:00:00 2001 From: KeremP Date: Thu, 14 Jul 2022 20:55:27 -0400 Subject: [PATCH 19/32] minor cleanups --- uniswap/util.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/uniswap/util.py b/uniswap/util.py index 70c4525..39f1ad5 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -8,13 +8,8 @@ from web3.exceptions import NameNotFound from web3.contract import Contract -<<<<<<< HEAD from .constants import MIN_TICK, MAX_TICK, _tick_spacing -from .types import AddressLike, Address, Contract -======= from .types import AddressLike, Address ->>>>>>> upstream/master - def _str_to_addr(s: Union[AddressLike, str]) -> Address: """Idempotent""" From 30e9759cb2bddceb890550bca603f583afdea6a0 Mon Sep 17 00:00:00 2001 From: KeremP Date: Thu, 14 Jul 2022 21:14:43 -0400 Subject: [PATCH 20/32] fixing minor errors afer rebase --- tests/test_uniswap.py | 7 +++---- uniswap/uniswap.py | 12 ++++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 822759a..e884dd5 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -8,7 +8,6 @@ from contextlib import contextmanager from dataclasses import dataclass from time import sleep -from requests import get from web3 import Web3 from web3.exceptions import NameNotFound @@ -249,7 +248,7 @@ def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, token0, token1, am pool = client.get_pool_instance(token0, token1, fee) # Ensuring client has sufficient balance of both tokens - eth_to_dai = client.make_trade(ETH_ADDRESS, token0, qty, None) + eth_to_dai = client.make_trade(get_tokens('mainnet')['ETH'], token0, qty, None) eth_to_dai_tx = client.w3.eth.wait_for_transaction_receipt(eth_to_dai, timeout=RECEIPT_TIMEOUT) dai_to_usdc = client.make_trade(token0, token1, amount1, None) @@ -264,7 +263,7 @@ def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, token0, token1, am tick_upper=max_tick, deadline=2**64 ) - assert r.status == 1 + assert r["status"] position_balance = client.nonFungiblePositionManager.functions.balanceOf(_addr_to_str(client.address)).call() assert position_balance > 0 @@ -282,7 +281,7 @@ def test_close_position(self, client: Uniswap, deadline): position_array = client.get_liquidity_positions() tokenId = position_array[0] r = client.close_position(tokenId, deadline=deadline) - assert r.status == 1 + assert r["status"] @pytest.mark.skip @pytest.mark.parametrize( diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 1a49356..11206d5 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -17,7 +17,7 @@ from eth_typing.evm import Address, ChecksumAddress from hexbytes import HexBytes -from .types import AddressLike, Contract +from .types import AddressLike from .token import ERC20Token from .tokens import get_tokens from .exceptions import InvalidToken, InsufficientBalance @@ -1181,11 +1181,11 @@ def close_position( return receipt - def get_token0_in_pool(liquidity: float, sqrtPrice: float, sqrtPriceLow: float, sqrtPriceHigh: float) -> float: + 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(liquidity: float, sqrtPrice: float, sqrtPriceLow: float, sqrtPriceHigh: float) -> float: + 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) @@ -1195,9 +1195,9 @@ def get_tvl_in_pool(self, pool: Contract) -> Tuple[float,float]: fee = pool_immutables['fee'] sqrtPrice = pool_state['sqrtPricex96'] / (1 << 96) - token0_liquidity = 0 - token1_liquidity = 0 - liquidity_total = 0 + token0_liquidity = 0.0 + token1_liquidity = 0.0 + liquidity_total = 0.0 TICK_SPACING = _tick_spacing[fee] for tick in range(MIN_TICK, MAX_TICK, TICK_SPACING): tick_liquidity = pool.functions.ticks(tick).call() From 6fc06802387235280b82cda44ba3442f0bede2b0 Mon Sep 17 00:00:00 2001 From: KeremP Date: Thu, 14 Jul 2022 21:54:55 -0400 Subject: [PATCH 21/32] cleanup asset amounts for tests --- tests/test_uniswap.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index e884dd5..609fdb9 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -251,7 +251,7 @@ def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, token0, token1, am eth_to_dai = client.make_trade(get_tokens('mainnet')['ETH'], token0, qty, None) eth_to_dai_tx = client.w3.eth.wait_for_transaction_receipt(eth_to_dai, timeout=RECEIPT_TIMEOUT) - dai_to_usdc = client.make_trade(token0, token1, amount1, None) + dai_to_usdc = client.make_trade(token0, token1, amount1*2, None) dai_to_usdc_tx = client.w3.eth.wait_for_transaction_receipt(dai_to_usdc, timeout=RECEIPT_TIMEOUT) min_tick, max_tick = default_tick_range(fee) @@ -323,7 +323,7 @@ def test_remove_liquidity( # Token -> Token ("DAI", "USDC", ONE_ETH, None, does_not_raise), # Token -> ETH - ("USDC", "ETH", 100 * ONE_USDC, None, does_not_raise), + ("USDC", "ETH", ONE_USDC, 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), @@ -362,11 +362,11 @@ def test_make_trade( "input_token, output_token, qty, recipient, expectation", [ # ETH -> Token - ("ETH", "DAI", 10 ** 18, None, does_not_raise), + ("ETH", "DAI", ONE_ETH, None, does_not_raise), # Token -> Token ("DAI", "USDC", ONE_USDC, None, does_not_raise), # Token -> ETH - ("DAI", "ETH", 10 ** 16, None, does_not_raise), + ("DAI", "ETH", 100 * ONE_USDC, 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), @@ -374,7 +374,7 @@ def test_make_trade( ( "DAI", "ETH", - 10 * 10 ** 18, + 10 * ONE_ETH, None, lambda: pytest.raises(InsufficientBalance), ), From 2f960ae35a93d27d838fc96e834acede381d9808 Mon Sep 17 00:00:00 2001 From: KeremP Date: Fri, 15 Jul 2022 01:28:48 -0400 Subject: [PATCH 22/32] fix: ensure asset amounts are correct for v3 liquidity position tests --- tests/test_uniswap.py | 26 ++++++++++++++++++-------- uniswap/uniswap.py | 18 ++++++++++-------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 609fdb9..f3e3230 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -235,24 +235,33 @@ def test_get_exchange_rate( @pytest.mark.parametrize( "token0, token1, amount0, amount1, qty, fee", [ - (get_tokens('mainnet')['DAI'], get_tokens('mainnet')['USDC'], ONE_ETH, ONE_USDC, ONE_ETH, 3000), + ('DAI', 'USDC', ONE_ETH, ONE_USDC, ONE_ETH, 3000), ] ) - def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, token0, token1, amount0, amount1, qty, fee): + 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(token0, token1, fee) + pool = client.create_pool_instance(tokens[token0], tokens[token1], fee) except Exception: - pool = client.get_pool_instance(token0, token1, fee) - + 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(get_tokens('mainnet')['ETH'], token0, qty, None) + eth_to_dai = client.make_trade(tokens['ETH'], tokens[token0], qty, client.address) eth_to_dai_tx = client.w3.eth.wait_for_transaction_receipt(eth_to_dai, timeout=RECEIPT_TIMEOUT) - - dai_to_usdc = client.make_trade(token0, token1, amount1*2, None) + assert eth_to_dai_tx["status"] + dai_to_usdc = client.make_trade(tokens[token0], tokens[token1], qty*10, client.address) 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( @@ -271,6 +280,7 @@ def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, token0, token1, am position_array = client.get_liquidity_positions() assert len(position_array) > 0 + @pytest.mark.parametrize( "deadline", [(2**64)], diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 11206d5..09aeefb 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1094,10 +1094,9 @@ def mint_liquidity( """ add liquidity to pool and mint position nft """ - address = _addr_to_str(self.address) + 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 ) @@ -1105,8 +1104,11 @@ def mint_liquidity( self.w3, abi_name="erc20", address=token_1 ) - assert token_0_instance.functions.balanceOf(address).call() > amount_0 - assert token_1_instance.functions.balanceOf(address).call() > amount_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) @@ -1117,11 +1119,11 @@ def mint_liquidity( # 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':address}) + 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':address}) - token_1_instance.functions.approve(nft_manager.address, amount_1).transact({'from':address}) + 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( @@ -1138,7 +1140,7 @@ def mint_liquidity( self.address, deadline ) - ).transact({'from':address}) + ).transact({'from':_addr_to_str(self.address)}) receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash) return receipt From 6ab2c51a12904347ed05c3e735f0d03c64b2dcb9 Mon Sep 17 00:00:00 2001 From: KeremP Date: Fri, 15 Jul 2022 02:48:08 -0400 Subject: [PATCH 23/32] add tests for TVL calculations. include method for fetching TVL from V3 subgraph as on-chain method takes long to run --- tests/test_uniswap.py | 27 +++++++++++++++++++++++++++ uniswap/constants.py | 4 +++- uniswap/uniswap.py | 34 +++++++++++++++++++++++++++++++--- uniswap/util.py | 13 ++++++++++++- 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index f3e3230..254712c 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -292,6 +292,33 @@ def test_close_position(self, client: Uniswap, deadline): tokenId = position_array[0] r = client.close_position(tokenId, deadline=deadline) assert r["status"] + + @pytest.mark.skip + @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]) + amount0, amount1 = client.get_tvl_in_pool_on_chain(pool) + print(amount0, amount1) + assert amount0 + assert amount1 + + @pytest.mark.parametrize( + "token0, token1", + [("DAI","USDC")] + ) + def test_get_tvl_in_pool_graph(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]) + amount0, amount1 = client.get_tvl_in_pool_graph(pool) + assert amount0 + assert amount1 @pytest.mark.skip @pytest.mark.parametrize( diff --git a/uniswap/constants.py b/uniswap/constants.py index 1e60346..bdedb01 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -68,4 +68,6 @@ 500: 10, 3_000: 60, 10_000: 200 -} \ No newline at end of file +} + +UNISWAP_GRAPH_URL = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3" \ No newline at end of file diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 09aeefb..2d9cd02 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -30,12 +30,14 @@ encode_sqrt_ratioX96, is_same_address, nearest_tick, + run_query, ) from .decorators import supports, check_approval from .constants import ( MAX_TICK, MAX_UINT_128, MIN_TICK, + UNISWAP_GRAPH_URL, WETH9_ADDRESS, _netid_to_name, _factory_contract_addresses_v1, @@ -1191,11 +1193,16 @@ def get_token1_in_pool(self, liquidity: float, sqrtPrice: float, sqrtPriceLow: f sqrtPrice = max(min(sqrtPrice, sqrtPriceHigh), sqrtPriceLow) return liquidity * (sqrtPrice - sqrtPriceLow) - def get_tvl_in_pool(self, pool: Contract) -> Tuple[float,float]: + # NOTE: this takes a while to run. + # Likely due to having to wait for contract function call to return on each iteration. + def get_tvl_in_pool_on_chain(self, pool: Contract) -> Tuple[float,float]: + """ + Iterate through each tick in a pool and calculate the TVL on-chain + """ pool_immutables = self.get_pool_immutables(pool) pool_state = self.get_pool_state(pool) fee = pool_immutables['fee'] - sqrtPrice = pool_state['sqrtPricex96'] / (1 << 96) + sqrtPrice = pool_state['sqrtPriceX96'] / (1 << 96) token0_liquidity = 0.0 token1_liquidity = 0.0 @@ -1203,12 +1210,33 @@ def get_tvl_in_pool(self, pool: Contract) -> Tuple[float,float]: TICK_SPACING = _tick_spacing[fee] for tick in range(MIN_TICK, MAX_TICK, TICK_SPACING): tick_liquidity = pool.functions.ticks(tick).call() - liquidity_total += tick_liquidity.liquidityNet + liquidity_total += tick_liquidity[1] # 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) return (token0_liquidity, token1_liquidity) + + def get_tvl_in_pool_graph(self, pool: Contract) -> Any: + """ + Make request to Uniswap V3 SubGraph endpoint to fetch TVL values + """ + pool_address = pool.address.lower() # for some reason subgraph queries don't like checksum addresses + query = f""" + {{ + pool(id: "{pool_address}") {{ + totalValueLockedToken0 + totalValueLockedToken1 + }} + + }} + """ + + response = run_query(query, UNISWAP_GRAPH_URL) + assert response['data']['pool'] is not None, 'Error retrieving pool data' + amount0 = response['data']['pool']['totalValueLockedToken0'] + amount1 = response['data']['pool']['totalValueLockedToken1'] + return amount0, amount1 # ------ Approval Utils ------------------------------------------------------------ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: diff --git a/uniswap/util.py b/uniswap/util.py index 39f1ad5..0f95b2f 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -2,7 +2,8 @@ import json import math import functools -from typing import Union, List, Tuple +from typing import Any, Dict, Union, List, Tuple +import requests from web3 import Web3 from web3.exceptions import NameNotFound @@ -102,3 +103,13 @@ def nearest_tick(tick: int, fee: int) -> int: return rounded_tick_spacing - tick_spacing else: return rounded_tick_spacing + +# Make requests to theGraph endpoint +def run_query(query: str, graph_url: str) -> Any: + request = requests.post(graph_url, json={'query':query}) + + if request.status_code == 200: + return request.json() + else: + raise Exception(f'Query returned code: {request.status_code}') + From ed43b4b12ac7d59f2b08d39afa18c075d2016aa9 Mon Sep 17 00:00:00 2001 From: KeremP Date: Fri, 15 Jul 2022 03:00:38 -0400 Subject: [PATCH 24/32] add arbitrum subgraph endpoint --- uniswap/constants.py | 5 ++++- uniswap/uniswap.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/uniswap/constants.py b/uniswap/constants.py index bdedb01..8783689 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -70,4 +70,7 @@ 10_000: 200 } -UNISWAP_GRAPH_URL = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3" \ No newline at end of file +UNISWAP_GRAPH_URL = { + "mainnet":"https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3", + "arbitrum":"https://api.thegraph.com/subgraphs/name/0xadsyst/uniswap-positions-arbitrum" + } \ No newline at end of file diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 2d9cd02..dbde80c 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1231,8 +1231,9 @@ def get_tvl_in_pool_graph(self, pool: Contract) -> Any: }} """ - - response = run_query(query, UNISWAP_GRAPH_URL) + chain = self.w3.eth.cahin_id + network = _netid_to_name[chain] + response = run_query(query, UNISWAP_GRAPH_URL[network]) assert response['data']['pool'] is not None, 'Error retrieving pool data' amount0 = response['data']['pool']['totalValueLockedToken0'] amount1 = response['data']['pool']['totalValueLockedToken1'] From 6f0ff5b506eaac81f3b7edbde4ed6bb17a9fa86d Mon Sep 17 00:00:00 2001 From: KeremP Date: Fri, 15 Jul 2022 03:03:04 -0400 Subject: [PATCH 25/32] fix typo --- uniswap/uniswap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index dbde80c..a9bd963 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1231,7 +1231,7 @@ def get_tvl_in_pool_graph(self, pool: Contract) -> Any: }} """ - chain = self.w3.eth.cahin_id + chain = self.w3.eth.chain_id network = _netid_to_name[chain] response = run_query(query, UNISWAP_GRAPH_URL[network]) assert response['data']['pool'] is not None, 'Error retrieving pool data' From 30cce24e8154ffe73b74252adb3a8123406a12fc Mon Sep 17 00:00:00 2001 From: KeremP Date: Sat, 16 Jul 2022 21:14:03 -0400 Subject: [PATCH 26/32] skip tvl tests for now --- tests/test_uniswap.py | 1 + uniswap/uniswap.py | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 254712c..a348ead 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -308,6 +308,7 @@ def test_get_tvl_in_pool_on_chain(self, client: Uniswap, tokens, token0, token1) assert amount0 assert amount1 + @pytest.mark.skip @pytest.mark.parametrize( "token0, token1", [("DAI","USDC")] diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index a9bd963..9e6398f 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1231,9 +1231,8 @@ def get_tvl_in_pool_graph(self, pool: Contract) -> Any: }} """ - chain = self.w3.eth.chain_id - network = _netid_to_name[chain] - response = run_query(query, UNISWAP_GRAPH_URL[network]) + + response = run_query(query, UNISWAP_GRAPH_URL[self.netname]) assert response['data']['pool'] is not None, 'Error retrieving pool data' amount0 = response['data']['pool']['totalValueLockedToken0'] amount1 = response['data']['pool']['totalValueLockedToken1'] From 1e5d430b05fae6985a8ada32475090320b1b7966 Mon Sep 17 00:00:00 2001 From: KeremP Date: Thu, 18 Aug 2022 15:48:50 -0400 Subject: [PATCH 27/32] feat: get TVL on chain --- Makefile | 2 +- tests/test_uniswap.py | 21 +- uniswap/assets/uniswap-v3/multicall.abi | 313 ++++++++++++++++++++++++ uniswap/constants.py | 6 + uniswap/uniswap.py | 182 +++++++++++--- uniswap/util.py | 94 ++++++- 6 files changed, 562 insertions(+), 56 deletions(-) create mode 100644 uniswap/assets/uniswap-v3/multicall.abi diff --git a/Makefile b/Makefile index aa3e4d5..75a49d5 100644 --- a/Makefile +++ b/Makefile @@ -21,4 +21,4 @@ precommit: make test docs: - cd docs/ && make html + cd docs/ && make html \ No newline at end of file diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index a348ead..d7b1abf 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -293,7 +293,7 @@ def test_close_position(self, client: Uniswap, deadline): r = client.close_position(tokenId, deadline=deadline) assert r["status"] - @pytest.mark.skip + # @pytest.mark.skip @pytest.mark.parametrize( "token0, token1", [("DAI", "USDC")] @@ -303,23 +303,8 @@ def test_get_tvl_in_pool_on_chain(self, client: Uniswap, tokens, token0, token1) pytest.skip("Not supported in this version of Uniswap") pool = client.get_pool_instance(tokens[token0], tokens[token1]) - amount0, amount1 = client.get_tvl_in_pool_on_chain(pool) - print(amount0, amount1) - assert amount0 - assert amount1 - - @pytest.mark.skip - @pytest.mark.parametrize( - "token0, token1", - [("DAI","USDC")] - ) - def test_get_tvl_in_pool_graph(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]) - amount0, amount1 = client.get_tvl_in_pool_graph(pool) - assert amount0 - assert amount1 + resp = client.get_tvl_in_pool(pool) + assert resp @pytest.mark.skip @pytest.mark.parametrize( diff --git a/uniswap/assets/uniswap-v3/multicall.abi b/uniswap/assets/uniswap-v3/multicall.abi new file mode 100644 index 0000000..2760624 --- /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/constants.py b/uniswap/constants.py index 8783689..bbfe970 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -70,6 +70,12 @@ 10_000: 200 } +_tick_bitmap_range = { + 500: (-347, 346), + 3_000: (-58, 57), + 10_000: (-18, 17) +} + UNISWAP_GRAPH_URL = { "mainnet":"https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3", "arbitrum":"https://api.thegraph.com/subgraphs/name/0xadsyst/uniswap-positions-arbitrum" diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 9e6398f..041e3d9 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1,11 +1,14 @@ -from multiprocessing import pool +from collections import namedtuple import os import time import logging import functools -from typing import List, Any, Optional, Union, Tuple, Iterable, Dict +from typing import List, Any, Optional, Sequence, Union, Tuple, Iterable, Dict +from xml.etree.ElementPath import find from web3 import Web3 +from web3._utils.abi import map_abi_data +from web3._utils.normalizers import BASE_RETURN_NORMALIZERS from web3.contract import Contract, ContractFunction from web3.exceptions import BadFunctionCallOutput, ContractLogicError from web3.types import ( @@ -27,10 +30,13 @@ _validate_address, _load_contract, _load_contract_erc20, + chunks, encode_sqrt_ratioX96, + get_max_tick, is_same_address, nearest_tick, run_query, + nextInitializedTickWithinOneWord, ) from .decorators import supports, check_approval from .constants import ( @@ -44,6 +50,7 @@ _factory_contract_addresses_v2, _router_contract_addresses_v2, _tick_spacing, + _tick_bitmap_range, ETH_ADDRESS, ) @@ -183,6 +190,10 @@ def __init__( self.nonFungiblePositionManager = _load_contract( self.w3, abi_name="uniswap-v3/nonFungiblePositionManager", address=self.positionManager_addr ) + multicall2_addr = _str_to_addr("0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696") + self.multicall2 = _load_contract( + self.w3, abi_name="uniswap-v3/multicall", address=multicall2_addr + ) else: raise Exception( f"Invalid version '{self.version}', only 1, 2 or 3 supported" @@ -1185,6 +1196,7 @@ def close_position( 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) @@ -1192,13 +1204,80 @@ def get_token0_in_pool(self, liquidity: float, sqrtPrice: float, sqrtPriceLow: f 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) - - # NOTE: this takes a while to run. - # Likely due to having to wait for contract function call to return on each iteration. - def get_tvl_in_pool_on_chain(self, pool: Contract) -> Tuple[float,float]: + + + # 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, bitmap, tick_spacing, fee): + 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, tick_spacing, fee): + 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, pool, tick_spacing, fee, left=True): + # 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 + + 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'] @@ -1207,36 +1286,44 @@ def get_tvl_in_pool_on_chain(self, pool: Contract) -> Tuple[float,float]: token0_liquidity = 0.0 token1_liquidity = 0.0 liquidity_total = 0.0 + TICK_SPACING = _tick_spacing[fee] - for tick in range(MIN_TICK, MAX_TICK, TICK_SPACING): - tick_liquidity = pool.functions.ticks(tick).call() - liquidity_total += tick_liquidity[1] # 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) + + 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) + + 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) - - def get_tvl_in_pool_graph(self, pool: Contract) -> Any: - """ - Make request to Uniswap V3 SubGraph endpoint to fetch TVL values - """ - pool_address = pool.address.lower() # for some reason subgraph queries don't like checksum addresses - query = f""" - {{ - pool(id: "{pool_address}") {{ - totalValueLockedToken0 - totalValueLockedToken1 - }} - - }} - """ - - response = run_query(query, UNISWAP_GRAPH_URL[self.netname]) - assert response['data']['pool'] is not None, 'Error retrieving pool data' - amount0 = response['data']['pool']['totalValueLockedToken0'] - amount1 = response['data']['pool']['totalValueLockedToken1'] - return amount0, amount1 # ------ Approval Utils ------------------------------------------------------------ def approve(self, token: AddressLike, max_approval: Optional[int] = None) -> None: @@ -1387,6 +1474,33 @@ def _calculate_max_output_token( # ------ Helpers ------------------------------------------------------------ + # 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] + ) -> Tuple[int, List[Optional[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 = self.multicall2.functions.aggregate(params).call(block_identifier="latest") + decoded_results = [self.w3.codec.decode_abi(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. diff --git a/uniswap/util.py b/uniswap/util.py index 0f95b2f..6f3c34e 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -2,7 +2,7 @@ import json import math import functools -from typing import Any, Dict, Union, List, Tuple +from typing import Any, Dict, Iterable, Sequence, Union, List, Tuple import requests from web3 import Web3 @@ -104,8 +104,8 @@ def nearest_tick(tick: int, fee: int) -> int: else: return rounded_tick_spacing -# Make requests to theGraph endpoint -def run_query(query: str, graph_url: str) -> Any: +# Make requests to graphql endpoint +def run_query(query: str, graph_url: str = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3") -> Any: request = requests.post(graph_url, json={'query':query}) if request.status_code == 200: @@ -113,3 +113,91 @@ def run_query(query: str, graph_url: str) -> Any: else: raise Exception(f'Query returned code: {request.status_code}') +def binary_search_ticks(ticks:List[int], tick:int) -> int: + """ + Find largest tick in tick array that is less than or equal to tick. + Returns index of found tick. + """ + assert tick > ticks[0], "BELOW_SMALLEST_TICK" + + l = 0 + r = len(ticks)-1 + while True: + i = math.floor((l+r)/2) + + if ticks[i] <= tick and (i == len(ticks)-1 or ticks[i+1] > tick): + return i + + if ticks[i] < tick: + l = i+1 + else: + r = i -1 + +def nextInitializedTick( + ticks: List[int], + tick: int, + lte:bool + ) -> int: + + if lte: + assert tick > ticks[0], "BELOW_SMALLEST_TICK" + if tick >= ticks[-1]: + return ticks[-1] + index = binary_search_ticks(ticks, tick) + return ticks[index] + else: + assert tick <= ticks[-1], "AT_OR_ABOVE_LARGEST_TICK" + if tick < ticks[0]: + return ticks[0] + index = binary_search_ticks(ticks, tick) + return ticks[index+1] + +def getWordPos(tick: int) -> Tuple[int, int]: + wordPos = tick >> 8 + bitPos = tick % 256 + return (wordPos, bitPos) + +def nextInitializedTickWithinOneWord( + ticks: List[int], + tick: int, + lte: bool, + tickSpacing:int + ) -> Tuple[int, bool]: + + compressed = math.floor(tick/tickSpacing) + + wordPos, bitPos = getWordPos(tick) + # all 1 bits at or to the left of current bit position + mask = (1 << bitPos) - 1 + (1 << bitPos) + masked = ticks[wordPos] and mask + + init = masked != 0 + + if init: + next = (compressed - ()) + + # if lte : + # wordPos = compressed >> 8 + # minimum = (wordPos << 8) * tickSpacing + + # if tick < ticks[0]: + # return (minimum, False) + + # index = nextInitializedTick(ticks, tick, lte) + # nextInitTick = max(minimum, index) + # return (nextInitTick, nextInitTick == index) + + # else: + # wordPos = (compressed + 1) >> 8 + # maximum = (((wordPos +1) << 8) - 1) * tickSpacing + + # if tick >= ticks[-1]: + # return (maximum, False) + + # index = nextInitializedTick(ticks, tick, lte) + # nextInitTick = min(maximum, index) + # return (nextInitTick, nextInitTick == index) + +def chunks(arr: Iterable[any], n: int): + for i in range(0, len(arr), n): + yield arr[i:i+n] From 2604d4ce9ae356f189070e3b4046c151734cabd6 Mon Sep 17 00:00:00 2001 From: KeremP Date: Thu, 18 Aug 2022 15:51:25 -0400 Subject: [PATCH 28/32] minor cleanups --- uniswap/uniswap.py | 3 -- uniswap/util.py | 95 ---------------------------------------------- 2 files changed, 98 deletions(-) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 041e3d9..ef6681c 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -4,7 +4,6 @@ import logging import functools from typing import List, Any, Optional, Sequence, Union, Tuple, Iterable, Dict -from xml.etree.ElementPath import find from web3 import Web3 from web3._utils.abi import map_abi_data @@ -35,8 +34,6 @@ get_max_tick, is_same_address, nearest_tick, - run_query, - nextInitializedTickWithinOneWord, ) from .decorators import supports, check_approval from .constants import ( diff --git a/uniswap/util.py b/uniswap/util.py index 6f3c34e..fd8efc0 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -3,7 +3,6 @@ import math import functools from typing import Any, Dict, Iterable, Sequence, Union, List, Tuple -import requests from web3 import Web3 from web3.exceptions import NameNotFound @@ -104,100 +103,6 @@ def nearest_tick(tick: int, fee: int) -> int: else: return rounded_tick_spacing -# Make requests to graphql endpoint -def run_query(query: str, graph_url: str = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3") -> Any: - request = requests.post(graph_url, json={'query':query}) - - if request.status_code == 200: - return request.json() - else: - raise Exception(f'Query returned code: {request.status_code}') - -def binary_search_ticks(ticks:List[int], tick:int) -> int: - """ - Find largest tick in tick array that is less than or equal to tick. - Returns index of found tick. - """ - assert tick > ticks[0], "BELOW_SMALLEST_TICK" - - l = 0 - r = len(ticks)-1 - while True: - i = math.floor((l+r)/2) - - if ticks[i] <= tick and (i == len(ticks)-1 or ticks[i+1] > tick): - return i - - if ticks[i] < tick: - l = i+1 - else: - r = i -1 - -def nextInitializedTick( - ticks: List[int], - tick: int, - lte:bool - ) -> int: - - if lte: - assert tick > ticks[0], "BELOW_SMALLEST_TICK" - if tick >= ticks[-1]: - return ticks[-1] - index = binary_search_ticks(ticks, tick) - return ticks[index] - else: - assert tick <= ticks[-1], "AT_OR_ABOVE_LARGEST_TICK" - if tick < ticks[0]: - return ticks[0] - index = binary_search_ticks(ticks, tick) - return ticks[index+1] - -def getWordPos(tick: int) -> Tuple[int, int]: - wordPos = tick >> 8 - bitPos = tick % 256 - return (wordPos, bitPos) - -def nextInitializedTickWithinOneWord( - ticks: List[int], - tick: int, - lte: bool, - tickSpacing:int - ) -> Tuple[int, bool]: - - compressed = math.floor(tick/tickSpacing) - - wordPos, bitPos = getWordPos(tick) - # all 1 bits at or to the left of current bit position - mask = (1 << bitPos) - 1 + (1 << bitPos) - masked = ticks[wordPos] and mask - - init = masked != 0 - - if init: - next = (compressed - ()) - - # if lte : - # wordPos = compressed >> 8 - # minimum = (wordPos << 8) * tickSpacing - - # if tick < ticks[0]: - # return (minimum, False) - - # index = nextInitializedTick(ticks, tick, lte) - # nextInitTick = max(minimum, index) - # return (nextInitTick, nextInitTick == index) - - # else: - # wordPos = (compressed + 1) >> 8 - # maximum = (((wordPos +1) << 8) - 1) * tickSpacing - - # if tick >= ticks[-1]: - # return (maximum, False) - - # index = nextInitializedTick(ticks, tick, lte) - # nextInitTick = min(maximum, index) - # return (nextInitTick, nextInitTick == index) - def chunks(arr: Iterable[any], n: int): for i in range(0, len(arr), n): yield arr[i:i+n] From e6f972c4bfadf043b30a7866c0dec8423d46d316 Mon Sep 17 00:00:00 2001 From: KeremP Date: Thu, 18 Aug 2022 16:59:29 -0400 Subject: [PATCH 29/32] fix aribitrum multicall2 address. clean up TVL test --- tests/test_uniswap.py | 6 +++--- uniswap/constants.py | 7 +------ uniswap/uniswap.py | 17 +++++++++++------ uniswap/util.py | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index d7b1abf..5493a09 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -293,7 +293,6 @@ def test_close_position(self, client: Uniswap, deadline): r = client.close_position(tokenId, deadline=deadline) assert r["status"] - # @pytest.mark.skip @pytest.mark.parametrize( "token0, token1", [("DAI", "USDC")] @@ -303,8 +302,9 @@ def test_get_tvl_in_pool_on_chain(self, client: Uniswap, tokens, token0, token1) pytest.skip("Not supported in this version of Uniswap") pool = client.get_pool_instance(tokens[token0], tokens[token1]) - resp = client.get_tvl_in_pool(pool) - assert resp + tvl_0, tvl_1 = client.get_tvl_in_pool(pool) + assert tvl_0 > 0 + assert tvl_1 > 0 @pytest.mark.skip @pytest.mark.parametrize( diff --git a/uniswap/constants.py b/uniswap/constants.py index bbfe970..0abefec 100644 --- a/uniswap/constants.py +++ b/uniswap/constants.py @@ -74,9 +74,4 @@ 500: (-347, 346), 3_000: (-58, 57), 10_000: (-18, 17) -} - -UNISWAP_GRAPH_URL = { - "mainnet":"https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3", - "arbitrum":"https://api.thegraph.com/subgraphs/name/0xadsyst/uniswap-positions-arbitrum" - } \ No newline at end of file +} \ No newline at end of file diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index ef6681c..9848b88 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -40,7 +40,6 @@ MAX_TICK, MAX_UINT_128, MIN_TICK, - UNISWAP_GRAPH_URL, WETH9_ADDRESS, _netid_to_name, _factory_contract_addresses_v1, @@ -187,7 +186,10 @@ def __init__( self.nonFungiblePositionManager = _load_contract( self.w3, abi_name="uniswap-v3/nonFungiblePositionManager", address=self.positionManager_addr ) - multicall2_addr = _str_to_addr("0x5BA1e12693Dc8F9c48aAD8770482f4739bEeD696") + 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 ) @@ -1204,7 +1206,7 @@ def get_token1_in_pool(self, liquidity: float, sqrtPrice: float, sqrtPriceLow: f # 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, bitmap, tick_spacing, fee): + 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) @@ -1212,14 +1214,14 @@ def get_max_tick_from_wordpos(self, wordPos, bitmap, tick_spacing, fee): 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, tick_spacing, fee): + 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, pool, tick_spacing, fee, left=True): + def find_tick_from_bitmap(self, bitmap_spacing: Tuple[int, int], pool: Contract, tick_spacing: int, fee: int, left: bool = True) -> Union[int, bool]: # searching to the left (finding max tick) if left: min_wordPos = bitmap_spacing[1] @@ -1251,6 +1253,7 @@ def find_tick_from_bitmap(self, bitmap_spacing, pool, tick_spacing, fee, left=Tr 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]: """ @@ -1290,6 +1293,8 @@ def get_tvl_in_pool(self, pool: Contract) -> Tuple[float,float]: _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 != False, "Error finding max tick" + assert _min_tick != False, "Error finding min tick" Batch = namedtuple("Batch", "ticks batchResults") ticks = [] @@ -1476,7 +1481,7 @@ def multicall( self, encoded_functions:Sequence[Tuple[ChecksumAddress, bytes]], output_types: Sequence[str] - ) -> Tuple[int, List[Optional[Any]]]: + ) -> List[Any]: """ Calls aggregate() on Uniswap Multicall2 contract diff --git a/uniswap/util.py b/uniswap/util.py index fd8efc0..10b664e 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -2,7 +2,7 @@ import json import math import functools -from typing import Any, Dict, Iterable, Sequence, Union, List, Tuple +from typing import Any, Dict, Generator, Iterable, Sequence, Union, List, Tuple from web3 import Web3 from web3.exceptions import NameNotFound @@ -103,6 +103,6 @@ def nearest_tick(tick: int, fee: int) -> int: else: return rounded_tick_spacing -def chunks(arr: Iterable[any], n: int): +def chunks(arr: Sequence[Any], n: int) -> Generator: for i in range(0, len(arr), n): yield arr[i:i+n] From 946853632e748280203ef5e46d5e46bc15340e2f Mon Sep 17 00:00:00 2001 From: KeremP Date: Fri, 26 Aug 2022 12:46:36 -0400 Subject: [PATCH 30/32] update get_liquidity_positions to take arbitrary address param --- uniswap/uniswap.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 9848b88..d44a136 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -1637,16 +1637,19 @@ def get_pool_state( return pool_state @supports([3]) - def get_liquidity_positions(self) -> List[int]: + def get_liquidity_positions(self, address: Optional[AddressLike] = None) -> List[int]: """ Enumerates liquidity position tokens owned by address. Returns array of token IDs. """ + if address is None: + address = self.address + positions: List[int] = [] - number_of_positions = self.nonFungiblePositionManager.functions.balanceOf(_addr_to_str(self.address)).call() + number_of_positions = self.nonFungiblePositionManager.functions.balanceOf(_addr_to_str(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() + position = self.nonFungiblePositionManager.functions.tokenOfOwnerByIndex(_addr_to_str(address), idx).call() positions.append(position) return positions From 812bb8765d0254c60fbc81882839626f47a0a3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Mon, 29 Aug 2022 10:01:38 +0200 Subject: [PATCH 31/32] fix: removed print in internal code --- uniswap/uniswap.py | 1 - 1 file changed, 1 deletion(-) diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index d44a136..4922b2d 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -459,7 +459,6 @@ def make_trade( input_token, qty, recipient, fee, slippage, fee_on_transfer ) else: - print(input_token) return self._token_to_token_swap_input( input_token, output_token, From 01752654f57815505dd75e17ffc24254135ccacd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Bj=C3=A4reholt?= Date: Mon, 29 Aug 2022 11:47:05 +0200 Subject: [PATCH 32/32] fix: fixed lint and bug in tests --- tests/test_uniswap.py | 90 ++++++++++++++++++++++++++----------------- uniswap/uniswap.py | 5 --- uniswap/util.py | 3 +- 3 files changed, 56 insertions(+), 42 deletions(-) diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index a8989c1..d72ebb4 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -16,7 +16,12 @@ from uniswap.constants import ETH_ADDRESS, WETH9_ADDRESS from uniswap.exceptions import InsufficientBalance from uniswap.tokens import get_tokens -from uniswap.util import _str_to_addr, default_tick_range, _addr_to_str, _load_contract_erc20 +from uniswap.util import ( + _str_to_addr, + default_tick_range, + _addr_to_str, + _load_contract_erc20, +) logger = logging.getLogger(__name__) @@ -197,10 +202,11 @@ def test_get_raw_price(self, client: Uniswap, tokens, token0, token1, fee): @pytest.mark.parametrize( "token0, token1, kwargs", [ - (weth, dai, {"fee": 500}), - ] + ("WETH", "DAI", {"fee": 500}), + ], ) - def test_get_pool_instance(self, client, token0, token1, kwargs): + 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) @@ -209,10 +215,11 @@ def test_get_pool_instance(self, client, token0, token1, kwargs): @pytest.mark.parametrize( "token0, token1, kwargs", [ - (weth, dai, {"fee": 500}), - ] + ("WETH", "DAI", {"fee": 500}), + ], ) - def test_get_pool_immutables(self, client, token0, token1, kwargs): + 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) @@ -223,10 +230,11 @@ def test_get_pool_immutables(self, client, token0, token1, kwargs): @pytest.mark.parametrize( "token0, token1, kwargs", [ - (weth, dai, {"fee": 500}), - ] + ("WETH", "DAI", {"fee": 500}), + ], ) - def test_get_pool_state(self, client, token0, token1, kwargs): + 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) @@ -237,10 +245,13 @@ def test_get_pool_state(self, client, token0, token1, kwargs): @pytest.mark.parametrize( "amount0, amount1, token0, token1, kwargs", [ - (1, 10, weth, dai, {"fee":500}), - ] + (1, 10, "WETH", "DAI", {"fee": 500}), + ], ) - def test_mint_position(self, client, amount0, amount1, token0, token1, kwargs): + 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) @@ -289,10 +300,12 @@ def test_get_exchange_rate( @pytest.mark.parametrize( "token0, token1, amount0, amount1, qty, fee", [ - ('DAI', 'USDC', ONE_ETH, ONE_USDC, ONE_ETH, 3000), - ] + ("DAI", "USDC", ONE_ETH, ONE_USDC, ONE_ETH, 3000), + ], ) - def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, tokens, token0, token1, amount0, amount1, qty, fee): + 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") @@ -303,41 +316,49 @@ def test_v3_deploy_pool_with_liquidity(self, client: Uniswap, tokens, token0, to print(pool.address) # Ensuring client has sufficient balance of both tokens - eth_to_dai = client.make_trade(tokens['ETH'], tokens[token0], qty, client.address) - eth_to_dai_tx = client.w3.eth.wait_for_transaction_receipt(eth_to_dai, timeout=RECEIPT_TIMEOUT) + eth_to_dai = client.make_trade( + tokens["ETH"], tokens[token0], qty, client.address + ) + 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) - dai_to_usdc_tx = client.w3.eth.wait_for_transaction_receipt(dai_to_usdc, timeout=RECEIPT_TIMEOUT) + dai_to_usdc = client.make_trade( + tokens[token0], tokens[token1], qty * 10, client.address + ) + 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}' - + 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 + 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() + 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)], + [(2 ** 64)], ) def test_close_position(self, client: Uniswap, deadline): if client.version != 3: @@ -347,10 +368,7 @@ def test_close_position(self, client: Uniswap, deadline): r = client.close_position(tokenId, deadline=deadline) assert r["status"] - @pytest.mark.parametrize( - "token0, token1", - [("DAI", "USDC")] - ) + @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") diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index fa47fb1..949224c 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -21,7 +21,6 @@ from .types import AddressLike from .token import ERC20Token -from .tokens import get_tokens from .exceptions import InvalidToken, InsufficientBalance from .util import ( _str_to_addr, @@ -31,15 +30,12 @@ _load_contract_erc20, chunks, encode_sqrt_ratioX96, - get_max_tick, is_same_address, nearest_tick, ) from .decorators import supports, check_approval from .constants import ( - MAX_TICK, MAX_UINT_128, - MIN_TICK, WETH9_ADDRESS, _netid_to_name, _factory_contract_addresses_v1, @@ -1737,7 +1733,6 @@ def mint_position(self, pool: Contract, amount0: int, amount1: int) -> None: MIN_TICK = -887272 MAX_TICK = -MIN_TICK - pool_sate = self.get_pool_state(pool) pool_immutables = self.get_pool_immutables(pool) token0 = pool_immutables["token0"] diff --git a/uniswap/util.py b/uniswap/util.py index e9fd676..2f65ab2 100644 --- a/uniswap/util.py +++ b/uniswap/util.py @@ -2,7 +2,7 @@ import json import math import functools -from typing import Any, Dict, Generator, Iterable, Sequence, Union, List, Tuple +from typing import Any, Generator, Sequence, Union, List, Tuple from web3 import Web3 from web3.exceptions import NameNotFound @@ -11,6 +11,7 @@ from .constants import MIN_TICK, MAX_TICK, _tick_spacing from .types import AddressLike, Address + def _str_to_addr(s: Union[AddressLike, str]) -> Address: """Idempotent""" if isinstance(s, str):