diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 23ac279..acb725e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,8 @@ jobs: - name: Install dependencies run: | poetry install - npm install -g ganache-cli + # TODO: Update to stable ganache when released! + npm install -g ganache@beta - name: Test env: PROVIDER: ${{ secrets.MAINNET_PROVIDER }} diff --git a/Makefile b/Makefile index e8a8eb8..ac1ebe7 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ .PHONY: test typecheck lint precommit docs test: - poetry run pytest -v --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/pyproject.toml b/pyproject.toml index 20d3052..be139bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,10 @@ Sphinx = "*" sphinx-book-theme = "*" sphinx-click = "*" +[tool.pytest.ini_options] +log_cli = false # to print logs during tests, set to true +#log_level = "NOTSET" + [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" diff --git a/tests/test_uniswap.py b/tests/test_uniswap.py index 3764772..08ea0b7 100644 --- a/tests/test_uniswap.py +++ b/tests/test_uniswap.py @@ -18,6 +18,7 @@ logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) ENV_UNISWAP_VERSION = os.getenv("UNISWAP_VERSION", None) if ENV_UNISWAP_VERSION: @@ -25,6 +26,8 @@ else: UNISWAP_VERSIONS = [1, 2, 3] +RECEIPT_TIMEOUT = 5 + @dataclass class GanacheInstance: @@ -36,7 +39,11 @@ class GanacheInstance: @pytest.fixture(scope="module", params=UNISWAP_VERSIONS) def client(request, web3: Web3, ganache: GanacheInstance): return Uniswap( - ganache.eth_address, ganache.eth_privkey, web3=web3, version=request.param + ganache.eth_address, + ganache.eth_privkey, + web3=web3, + version=request.param, + use_estimate_gas=False, # see note in _build_and_send_tx ) @@ -54,12 +61,12 @@ def test_assets(client: Uniswap): logger.info("Buying...") tx = client.make_trade_output(tokens["ETH"], token_addr, amount) - client.w3.eth.wait_for_transaction_receipt(tx) + client.w3.eth.wait_for_transaction_receipt(tx, timeout=RECEIPT_TIMEOUT) @pytest.fixture(scope="module") def web3(ganache: GanacheInstance): - w3 = Web3(Web3.HTTPProvider(ganache.provider, request_kwargs={"timeout": 60})) + w3 = Web3(Web3.HTTPProvider(ganache.provider, request_kwargs={"timeout": 30})) if 1 != int(w3.net.version): raise Exception("PROVIDER was not a mainnet provider, which the tests require") return w3 @@ -67,10 +74,10 @@ def web3(ganache: GanacheInstance): @pytest.fixture(scope="module") def ganache() -> Generator[GanacheInstance, None, None]: - """Fixture that runs ganache-cli which has forked off mainnet""" - if not shutil.which("ganache-cli"): + """Fixture that runs ganache which has forked off mainnet""" + if not shutil.which("ganache"): raise Exception( - "ganache-cli was not found in PATH, you can install it with `npm install -g ganache-cli`" + "ganache was not found in PATH, you can install it with `npm install -g ganache`" ) if "PROVIDER" not in os.environ: raise Exception( @@ -78,11 +85,22 @@ def ganache() -> Generator[GanacheInstance, None, None]: ) port = 10999 + defaultGasPrice = 1000_000_000_000 # 1000 gwei p = subprocess.Popen( - f"ganache-cli --port {port} -s test --networkId 1 --fork {os.environ['PROVIDER']}", + f"""ganache + --port {port} + --wallet.seed test + --chain.networkId 1 + --chain.chainId 1 + --fork.url {os.environ['PROVIDER']} + --miner.defaultGasPrice {defaultGasPrice} + --miner.legacyInstamine true + """.replace( + "\n", " " + ), shell=True, ) - # Address #1 when ganache is run with `-s test`, it starts with 100 ETH + # Address #1 when ganache is run with `--wallet.seed test`, it starts with 1000 ETH eth_address = "0x94e3361495bD110114ac0b6e35Ed75E77E6a6cFA" eth_privkey = "0x6f1313062db38875fb01ee52682cbf6a8420e92bfbc578c5d4fdc0a32c50266f" sleep(3) @@ -105,7 +123,7 @@ class TestUniswap(object): ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" # TODO: Detect mainnet vs rinkeby and set accordingly, like _get_token_addresses in the Uniswap class - # For Mainnet testing (with `ganache-cli --fork` as per the ganache fixture) + # For Mainnet testing (with `ganache --fork` as per the ganache fixture) eth = "0x0000000000000000000000000000000000000000" weth = Web3.toChecksumAddress("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2") bat = Web3.toChecksumAddress("0x0D8775F648430679A709E98d2b0Cb6250d2887EF") @@ -219,7 +237,7 @@ def get_exchange_rate( ) def test_add_liquidity(self, client: Uniswap, web3: Web3, token, max_eth): r = client.add_liquidity(token, max_eth) - tx = web3.eth.wait_for_transaction_receipt(r, timeout=6000) + tx = web3.eth.wait_for_transaction_receipt(r, timeout=RECEIPT_TIMEOUT) assert tx["status"] @pytest.mark.skip @@ -274,7 +292,7 @@ def test_make_trade( bal_in_before = client.get_token_balance(input_token) txid = client.make_trade(input_token, output_token, qty, recipient) - tx = web3.eth.wait_for_transaction_receipt(txid) + tx = web3.eth.wait_for_transaction_receipt(txid, timeout=RECEIPT_TIMEOUT) assert tx["status"] # TODO: Checks for ETH, taking gas into account @@ -324,7 +342,7 @@ def test_make_trade_output( balance_before = client.get_token_balance(output_token) r = client.make_trade_output(input_token, output_token, qty, recipient) - tx = web3.eth.wait_for_transaction_receipt(r, timeout=30) + tx = web3.eth.wait_for_transaction_receipt(r, timeout=RECEIPT_TIMEOUT) assert tx["status"] # TODO: Checks for ETH, taking gas into account diff --git a/uniswap/uniswap.py b/uniswap/uniswap.py index 6c54b70..fb543d6 100644 --- a/uniswap/uniswap.py +++ b/uniswap/uniswap.py @@ -45,6 +45,16 @@ class Uniswap: Wrapper around Uniswap contracts. """ + address: AddressLike + version: int + + w3: Web3 + netid: int + netname: str + + default_slippage: float + use_estimate_gas: bool + def __init__( self, address: Union[AddressLike, str, None], @@ -53,6 +63,8 @@ def __init__( web3: Web3 = None, version: int = 1, default_slippage: float = 0.01, + use_estimate_gas: bool = True, + # use_eip1559: bool = True, factory_contract_addr: str = None, router_contract_addr: str = None, ) -> None: @@ -66,7 +78,7 @@ def __init__( :param factory_contract_addr: Can be optionally set to override the address of the factory contract. :param router_contract_addr: Can be optionally set to override the address of the router contract (v2 only). """ - self.address: AddressLike = _str_to_addr( + self.address = _str_to_addr( address or "0x0000000000000000000000000000000000000000" ) self.private_key = ( @@ -78,22 +90,23 @@ def __init__( # TODO: Write tests for slippage self.default_slippage = default_slippage + self.use_estimate_gas = use_estimate_gas if web3: self.w3 = web3 else: # Initialize web3. Extra provider for testing. - self.provider = provider or os.environ["PROVIDER"] - self.w3 = Web3( - Web3.HTTPProvider(self.provider, request_kwargs={"timeout": 60}) - ) - - netid = int(self.w3.net.version) - if netid in _netid_to_name: - self.network = _netid_to_name[netid] + if not provider: + provider = os.environ["PROVIDER"] + self.w3 = Web3(Web3.HTTPProvider(provider, request_kwargs={"timeout": 60})) + + # Cache netid to avoid extra RPC calls + self.netid = int(self.w3.net.version) + if self.netid in _netid_to_name: + self.netname = _netid_to_name[self.netid] else: - raise Exception(f"Unknown netid: {netid}") - logger.info(f"Using {self.w3} ('{self.network}')") + raise Exception(f"Unknown netid: {self.netid}") + logger.info(f"Using {self.w3} ('{self.netname}', netid: {self.netid})") self.last_nonce: Nonce = self.w3.eth.get_transaction_count(self.address) @@ -102,14 +115,14 @@ def __init__( # max_approval_check checks that current approval is above a reasonable number # The program cannot check for max_approval each time because it decreases # with each trade. - self.max_approval_hex = f"0x{64 * 'f'}" - self.max_approval_int = int(self.max_approval_hex, 16) - self.max_approval_check_hex = f"0x{15 * '0'}{49 * 'f'}" - self.max_approval_check_int = int(self.max_approval_check_hex, 16) + max_approval_hex = f"0x{64 * 'f'}" + self.max_approval_int = int(max_approval_hex, 16) + max_approval_check_hex = f"0x{15 * '0'}{49 * 'f'}" + self.max_approval_check_int = int(max_approval_check_hex, 16) if self.version == 1: if factory_contract_addr is None: - factory_contract_addr = _factory_contract_addresses_v1[self.network] + factory_contract_addr = _factory_contract_addresses_v1[self.netname] self.factory_contract = _load_contract( self.w3, @@ -118,11 +131,11 @@ def __init__( ) elif self.version == 2: if router_contract_addr is None: - router_contract_addr = _router_contract_addresses_v2[self.network] + router_contract_addr = _router_contract_addresses_v2[self.netname] self.router_address: AddressLike = _str_to_addr(router_contract_addr) if factory_contract_addr is None: - factory_contract_addr = _factory_contract_addresses_v2[self.network] + factory_contract_addr = _factory_contract_addresses_v2[self.netname] self.factory_contract = _load_contract( self.w3, abi_name="uniswap-v2/factory", @@ -1085,8 +1098,19 @@ def _build_and_send_tx( if not tx_params: tx_params = self._get_tx_params() transaction = function.buildTransaction(tx_params) - # Uniswap3 uses 20% margin for transactions - transaction["gas"] = Wei(int(self.w3.eth.estimate_gas(transaction) * 1.2)) + + if "gas" not in tx_params: + # `use_estimate_gas` needs to be True for networks like Arbitrum (can't assume 250000 gas), + # but it breaks tests for unknown reasons because estimateGas takes forever on some tx's. + # Maybe an issue with ganache? (got GC warnings once...) + if self.use_estimate_gas: + # The Uniswap V3 UI uses 20% margin for transactions + transaction["gas"] = Wei( + int(self.w3.eth.estimate_gas(transaction) * 1.2) + ) + else: + transaction["gas"] = Wei(250000) + signed_txn = self.w3.eth.account.sign_transaction( transaction, private_key=self.private_key ) @@ -1098,15 +1122,18 @@ def _build_and_send_tx( logger.debug(f"nonce: {tx_params['nonce']}") self.last_nonce = Nonce(tx_params["nonce"] + 1) - def _get_tx_params(self, value: Wei = Wei(0)) -> TxParams: + def _get_tx_params(self, value: Wei = Wei(0), gas: Wei = None) -> TxParams: """Get generic transaction parameters.""" - return { + params: TxParams = { "from": _addr_to_str(self.address), "value": value, "nonce": max( self.last_nonce, self.w3.eth.get_transaction_count(self.address) ), } + if gas: + params["gas"] = gas + return params # ------ Price Calculation Utils --------------------------------------------------- def _calculate_max_input_token( @@ -1255,14 +1282,12 @@ def _get_token_addresses(self) -> Dict[str, ChecksumAddress]: Returns a dict with addresses for tokens for the current net. Used in testing. """ - netid = int(self.w3.net.version) - netname = _netid_to_name[netid] - if netname == "mainnet": + if self.netname == "mainnet": return tokens - elif netname == "rinkeby": + elif self.netname == "rinkeby": return tokens_rinkeby else: - raise Exception(f"Unknown net '{netname}'") + raise Exception(f"Unknown net '{self.netname}'") # ---- Old v1 utils ---- pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy