Merge pull request #2154 from cygnusv/mamma-mia

Improved gas price logic. Introduction of gas oracles
pull/2198/head
David Núñez 2020-08-28 18:13:43 +02:00 committed by GitHub
commit 60ebeacd64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 426 additions and 53 deletions

View File

@ -36,7 +36,7 @@ from geth.chain import (
is_ropsten_chain
)
from geth.process import BaseGethProcess
from typing import Union
from typing import Union, Optional
from web3 import Web3
from web3.contract import Contract
from web3.types import Wei, TxReceipt
@ -250,10 +250,13 @@ class EthereumClient:
return self.w3.eth.getBalance(account)
def inject_middleware(self, middleware, **kwargs):
return self.w3.middleware_onion.inject(middleware, **kwargs)
self.w3.middleware_onion.inject(middleware, **kwargs)
def add_middleware(self, middleware):
return self.w3.middleware_onion.add(middleware)
self.w3.middleware_onion.add(middleware)
def set_gas_strategy(self, gas_strategy):
self.w3.eth.setGasPriceStrategy(gas_strategy)
@property
def chain_id(self) -> int:
@ -273,8 +276,18 @@ class EthereumClient:
@property
def gas_price(self) -> Wei:
"""
Returns client's gas price. Underneath, it uses the eth_gasPrice JSON-RPC method
"""
return self.w3.eth.gasPrice
def gas_price_for_transaction(self, transaction=None) -> Wei:
"""
Obtains a gas price via the current gas strategy, if any; otherwise, it resorts to the client's gas price.
This method mirrors the behavior of web3._utils.transactions when building transactions.
"""
return self.w3.eth.generateGasPrice(transaction) or self.gas_price
@property
def block_number(self) -> BlockNumber:
return self.w3.eth.blockNumber

View File

@ -40,7 +40,7 @@ from web3.exceptions import TimeExhausted, ValidationError
from web3.gas_strategies import time_based
from web3.middleware import geth_poa_middleware
from nucypher.blockchain.eth.clients import EthereumClient, POA_CHAINS
from nucypher.blockchain.eth.clients import EthereumClient, POA_CHAINS, InfuraClient
from nucypher.blockchain.eth.decorators import validate_checksum_address
from nucypher.blockchain.eth.providers import (
_get_auto_provider,
@ -57,6 +57,7 @@ from nucypher.blockchain.eth.sol.compile import SolidityCompiler
from nucypher.blockchain.eth.utils import get_transaction_name, prettify_eth_amount
from nucypher.characters.control.emitters import JSONRPCStdoutEmitter, StdoutEmitter
from nucypher.utilities.logging import GlobalLoggerSettings, Logger
from nucypher.utilities.datafeeds import datafeed_fallback_gas_price_strategy
Web3Providers = Union[IPCProvider, WebsocketProvider, HTTPProvider, EthereumTester]
@ -73,7 +74,7 @@ class BlockchainInterface:
TIMEOUT = 600 # seconds # TODO: Correlate with the gas strategy - #2070
DEFAULT_GAS_STRATEGY = 'medium'
DEFAULT_GAS_STRATEGY = 'fast'
GAS_STRATEGIES = {'glacial': time_based.glacial_gas_price_strategy, # 24h
'slow': time_based.slow_gas_price_strategy, # 1h
'medium': time_based.medium_gas_price_strategy, # 5m
@ -253,32 +254,43 @@ class BlockchainInterface:
return self.client.is_connected
@classmethod
def get_gas_strategy(cls, gas_strategy: Union[str, Callable]) -> Callable:
def get_gas_strategy(cls, gas_strategy: Union[str, Callable] = None) -> Callable:
try:
gas_strategy = cls.GAS_STRATEGIES[gas_strategy]
except KeyError:
if gas_strategy and not callable(gas_strategy):
raise ValueError(f"{gas_strategy} must be callable to be a valid gas strategy.")
if gas_strategy:
if not callable(gas_strategy):
raise ValueError(f"{gas_strategy} must be callable to be a valid gas strategy.")
else:
gas_strategy = cls.GAS_STRATEGIES[cls.DEFAULT_GAS_STRATEGY]
return gas_strategy
def attach_middleware(self):
chain_id = int(self.client.chain_id)
if self.poa is None: # If POA is not set explicitly, try to autodetect from chain id
chain_id = int(self.client.chain_id)
self.poa = chain_id in POA_CHAINS
self.log.debug(f'Autodetecting POA chain ({self.client.chain_name})')
self.log.debug(f'Ethereum chain: {self.client.chain_name} (chain_id={chain_id}, poa={self.poa})')
# For use with Proof-Of-Authority test-blockchains
if self.poa is True:
self.log.debug('Injecting POA middleware at layer 0')
self.client.inject_middleware(geth_poa_middleware, layer=0)
# Gas Price Strategy
self.client.w3.eth.setGasPriceStrategy(self.gas_strategy)
self.client.w3.middleware_onion.add(middleware.time_based_cache_middleware)
self.client.w3.middleware_onion.add(middleware.latest_block_based_cache_middleware)
self.client.w3.middleware_onion.add(middleware.simple_cache_middleware)
# Gas Price Strategy:
# Bundled web3 strategies are too expensive for Infura (it takes ~1 minute to get a price),
# so we use external gas price oracles, instead (see #2139)
if isinstance(self.client, InfuraClient):
gas_strategy = datafeed_fallback_gas_price_strategy
else:
gas_strategy = self.gas_strategy
self.client.set_gas_strategy(gas_strategy=gas_strategy)
gwei_gas_price = Web3.fromWei(self.client.gas_price_for_transaction(), 'gwei')
self.log.debug(f"Currently, our gas strategy returns a gas price of {gwei_gas_price} gwei")
self.client.add_middleware(middleware.time_based_cache_middleware)
self.client.add_middleware(middleware.latest_block_based_cache_middleware)
self.client.add_middleware(middleware.simple_cache_middleware)
def connect(self):
@ -474,8 +486,7 @@ class BlockchainInterface:
base_payload = {'chainId': int(self.client.chain_id),
'nonce': self.client.w3.eth.getTransactionCount(sender_address, 'pending'),
'from': sender_address,
'gasPrice': self.client.gas_price}
'from': sender_address}
# Aggregate
if not payload:
@ -530,17 +541,22 @@ class BlockchainInterface:
# TODO: Show the USD Price: https://api.coinmarketcap.com/v1/ticker/ethereum/
price = transaction_dict['gasPrice']
price_gwei = Web3.fromWei(price, 'gwei')
cost_wei = price * transaction_dict['gas']
cost = Web3.fromWei(cost_wei, 'gwei')
cost = Web3.fromWei(cost_wei, 'ether')
if self.transacting_power.is_device:
emitter.message(f'Confirm transaction {transaction_name} on hardware wallet... ({cost} gwei @ {price})', color='yellow')
emitter.message(f'Confirm transaction {transaction_name} on hardware wallet... '
f'({cost} ETH @ {price_gwei} gwei)',
color='yellow')
signed_raw_transaction = self.transacting_power.sign_transaction(transaction_dict)
#
# Broadcast
#
emitter.message(f'Broadcasting {transaction_name} Transaction ({cost} gwei @ {price})...', color='yellow')
emitter.message(f'Broadcasting {transaction_name} Transaction ({cost} ETH @ {price_gwei} gwei)...',
color='yellow')
try:
txhash = self.client.send_raw_transaction(signed_raw_transaction) # <--- BROADCAST
except (TestTransactionFailed, ValueError) as error:

View File

@ -342,7 +342,7 @@ class Felix(Character, NucypherTokenActor):
transaction = {'to': recipient_address,
'from': self.checksum_address,
'value': ether,
'gasPrice': self.blockchain.client.gas_price}
'gasPrice': self.blockchain.client.gas_price_for_transaction()}
transaction_dict = self.blockchain.build_payload(sender_address=self.checksum_address,
payload=transaction,

View File

@ -105,7 +105,6 @@ class UrsulaConfigOptions:
message=f"--registry-filepath cannot be used in federated mode.")
eth_node = NO_BLOCKCHAIN_CONNECTION
provider_uri = provider_uri
if geth:
eth_node = get_geth_provider_process()
provider_uri = eth_node.provider_uri(scheme='file')

View File

@ -0,0 +1,120 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from abc import ABC, abstractmethod
from typing import Optional
import requests
from web3 import Web3
from web3.gas_strategies.rpc import rpc_gas_price_strategy
from web3.types import Wei, TxParams
class Datafeed(ABC):
class DatafeedError(RuntimeError):
"""Base class for exceptions concerning Datafeeds"""
name = NotImplemented
api_url = NotImplemented # TODO: Deal with API keys
def _probe_feed(self):
try:
response = requests.get(self.api_url)
except requests.exceptions.ConnectionError as e:
error = f"Failed to probe feed at {self.api_url}: {str(e)}"
raise self.DatafeedError(error)
if response.status_code != 200:
error = f"Failed to probe feed at {self.api_url} with status code {response.status_code}"
raise self.DatafeedError(error)
self._raw_data = response.json()
def __repr__(self):
return f"{self.name} ({self.api_url})"
class EthereumGasPriceDatafeed(Datafeed):
"""Base class for Ethereum gas price data feeds"""
_speed_names = NotImplemented
_default_speed = NotImplemented
@abstractmethod
def _parse_gas_prices(self):
return NotImplementedError
def get_gas_price(self, speed: Optional[str] = None) -> Wei:
speed = speed or self._default_speed
self._parse_gas_prices()
gas_price_wei = Wei(self.gas_prices[speed])
return gas_price_wei
@classmethod
def construct_gas_strategy(cls):
def gas_price_strategy(web3: Web3, transaction_params: TxParams = None) -> Wei:
feed = cls()
gas_price = feed.get_gas_price()
return gas_price
return gas_price_strategy
class EtherchainGasPriceDatafeed(EthereumGasPriceDatafeed):
"""Gas price datafeed from Etherchain"""
name = "Etherchain datafeed"
api_url = "https://www.etherchain.org/api/gasPriceOracle"
_speed_names = ('safeLow', 'standard', 'fast', 'fastest')
_default_speed = 'fast'
def _parse_gas_prices(self):
self._probe_feed()
self.gas_prices = {k: int(Web3.toWei(v, 'gwei')) for k, v in self._raw_data.items()}
class UpvestGasPriceDatafeed(EthereumGasPriceDatafeed):
"""Gas price datafeed from Upvest"""
name = "Upvest datafeed"
api_url = "https://fees.upvest.co/estimate_eth_fees"
_speed_names = ('slow', 'medium', 'fast', 'fastest')
_default_speed = 'fastest'
def _parse_gas_prices(self):
self._probe_feed()
self.gas_prices = {k: int(Web3.toWei(v, 'gwei')) for k, v in self._raw_data['estimates'].items()}
def datafeed_fallback_gas_price_strategy(web3: Web3, transaction_params: TxParams = None) -> Wei:
feeds = (EtherchainGasPriceDatafeed, UpvestGasPriceDatafeed)
for gas_price_feed_class in feeds:
try:
gas_strategy = gas_price_feed_class.construct_gas_strategy()
gas_price = gas_strategy(web3, transaction_params)
except Datafeed.DatafeedError:
continue
else:
return gas_price
else:
# Worst-case scenario, we get the price from the ETH node itself
return rpc_gas_price_strategy(web3, transaction_params)
# TODO: We can implement here other datafeeds, like the ETH/USD (e.g., https://api.coinmarketcap.com/v1/ticker/ethereum/)
# suggested in a comment in nucypher.blockchain.eth.interfaces.BlockchainInterface#sign_and_broadcast_transaction

View File

@ -16,7 +16,7 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from unittest.mock import PropertyMock, patch
from unittest.mock import patch
from nucypher.blockchain.eth.actors import Trustee
from nucypher.blockchain.eth.agents import MultiSigAgent
@ -39,11 +39,8 @@ def test_trustee_proposes_multisig_management_operations(testerchain, test_regis
trustee = Trustee(checksum_address=trustee_address, registry=test_registry)
# Propose changing threshold
# FIXME: I had to mock the gas_price property of EthereumTesterClient because I'm unable to set a free gas strategy
with patch('nucypher.blockchain.eth.clients.EthereumTesterClient.gas_price',
new_callable=PropertyMock) as mock_ethtester:
mock_ethtester.return_value = 0
free_payload = {'nonce': 0, 'from': multisig_agent.contract_address, 'gasPrice': 0}
with patch.object(testerchain, 'build_payload', return_value=free_payload):
proposal = trustee.propose_changing_threshold(new_threshold=1)
assert proposal.trustee_address == trustee_address
@ -57,9 +54,7 @@ def test_trustee_proposes_multisig_management_operations(testerchain, test_regis
# Propose adding new owner
new_owner = testerchain.unassigned_accounts[4]
with patch('nucypher.blockchain.eth.clients.EthereumTesterClient.gas_price',
new_callable=PropertyMock) as mock_ethtester:
mock_ethtester.return_value = 0
with patch.object(testerchain, 'build_payload', return_value=free_payload):
proposal = trustee.propose_adding_owner(new_owner_address=new_owner, evidence=None)
assert proposal.trustee_address == trustee_address
@ -73,9 +68,7 @@ def test_trustee_proposes_multisig_management_operations(testerchain, test_regis
# Propose removing owner
evicted_owner = testerchain.unassigned_accounts[1]
with patch('nucypher.blockchain.eth.clients.EthereumTesterClient.gas_price',
new_callable=PropertyMock) as mock_ethtester:
mock_ethtester.return_value = 0
with patch.object(testerchain, 'build_payload', return_value=free_payload):
proposal = trustee.propose_removing_owner(evicted_owner)
assert proposal.trustee_address == trustee_address

View File

@ -32,7 +32,7 @@ from nucypher.crypto.powers import TransactingPower
# Prevents TesterBlockchain to be picked up by py.test as a test class
from tests.fixtures import _make_testerchain
from tests.mock.interfaces import MockBlockchain
from tests.utils.blockchain import TesterBlockchain as _TesterBlockchain
from tests.utils.blockchain import TesterBlockchain as _TesterBlockchain, free_gas_price_strategy
from tests.constants import (DEVELOPMENT_ETH_AIRDROP_AMOUNT, INSECURE_DEVELOPMENT_PASSWORD,
NUMBER_OF_ETH_TEST_ACCOUNTS, NUMBER_OF_STAKERS_IN_BLOCKCHAIN_TESTS,
NUMBER_OF_URSULAS_IN_BLOCKCHAIN_TESTS)
@ -105,7 +105,9 @@ def test_multiversion_contract():
SourceDirs(root_dir, {v1_dir})])
# Prepare chain
blockchain_interface = BlockchainDeployerInterface(provider_uri='tester://pyevm/2', compiler=solidity_compiler)
blockchain_interface = BlockchainDeployerInterface(provider_uri='tester://pyevm/2',
compiler=solidity_compiler,
gas_strategy=free_gas_price_strategy)
blockchain_interface.connect()
origin = blockchain_interface.client.accounts[0]
blockchain_interface.transacting_power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD, account=origin)

View File

@ -22,7 +22,6 @@ import maya
import os
import pytest
import tempfile
from twisted.logger import Logger
from web3 import Web3
from nucypher.blockchain.eth.actors import Staker

View File

@ -28,6 +28,7 @@ from nucypher.blockchain.eth.registry import InMemoryContractRegistry
from nucypher.blockchain.eth.sol.compile import SolidityCompiler, SourceDirs
from nucypher.crypto.powers import TransactingPower
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD
from tests.utils.blockchain import free_gas_price_strategy
USER = "nucypher"
REPO = "nucypher"
@ -93,7 +94,7 @@ def deploy_earliest_contract(blockchain_interface: BlockchainDeployerInterface,
pass # Skip errors related to initialization
def test_upgradeability(temp_dir_path, token_economics):
def test_upgradeability(temp_dir_path):
# Prepare remote source for compilation
download_github_dir(GITHUB_SOURCE_LINK, temp_dir_path)
solidity_compiler = SolidityCompiler(source_dirs=[SourceDirs(SolidityCompiler.default_contract_dir()),
@ -102,7 +103,9 @@ def test_upgradeability(temp_dir_path, token_economics):
# Prepare the blockchain
provider_uri = 'tester://pyevm/2'
try:
blockchain_interface = BlockchainDeployerInterface(provider_uri=provider_uri, compiler=solidity_compiler)
blockchain_interface = BlockchainDeployerInterface(provider_uri=provider_uri,
compiler=solidity_compiler,
gas_strategy=free_gas_price_strategy)
blockchain_interface.connect()
origin = blockchain_interface.client.accounts[0]
BlockchainInterfaceFactory.register_interface(interface=blockchain_interface)

View File

@ -29,7 +29,6 @@ from click.testing import CliRunner
from datetime import datetime, timedelta
from eth_utils import to_checksum_address
from io import StringIO
from twisted.logger import Logger
from typing import Tuple
from umbral import pre
from umbral.curvebn import CurveBN

View File

@ -30,7 +30,7 @@ from nucypher.config.constants import (
NUCYPHER_ENVVAR_WORKER_ETH_PASSWORD,
TEMPORARY_DOMAIN
)
from tests.constants import MOCK_IP_ADDRESS, TEST_PROVIDER_URI
from tests.constants import MOCK_IP_ADDRESS
from tests.utils.ursula import MOCK_URSULA_STARTING_PORT
@ -63,7 +63,7 @@ def test_ursula_init_with_local_keystore_signer(click_runner,
'--network', TEMPORARY_DOMAIN,
'--worker-address', worker_account.address,
'--config-root', custom_filepath,
'--provider', TEST_PROVIDER_URI,
'--provider', mock_testerchain.provider_uri,
'--rest-host', MOCK_IP_ADDRESS,
'--rest-port', MOCK_URSULA_STARTING_PORT,

View File

@ -15,13 +15,10 @@ You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import click
import lmdb
import os
import pytest
from contextlib import contextmanager
from eth_account import Account
from eth_account.account import Account
from functools import partial
@ -86,10 +83,12 @@ class TestLMDBEnv:
def path(self):
return self.db_path
@pytest.fixture(autouse=True)
def JIT_lmdb_env(monkeypatch):
monkeypatch.setattr("lmdb.open", TestLMDBEnv)
@pytest.fixture(autouse=True)
def reduced_memory_page_lmdb(monkeypatch):
monkeypatch.setattr(Datastore, "LMDB_MAP_SIZE", 10_000_000)

View File

@ -27,8 +27,10 @@ from nucypher.blockchain.eth.agents import Agent, ContractAgency, EthereumContra
from nucypher.blockchain.eth.constants import NULL_ADDRESS
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
from tests.constants import MOCK_PROVIDER_URI
from tests.utils.blockchain import free_gas_price_strategy
MOCK_TESTERCHAIN = BlockchainInterfaceFactory.get_or_create_interface(provider_uri=MOCK_PROVIDER_URI)
MOCK_TESTERCHAIN = BlockchainInterfaceFactory.get_or_create_interface(provider_uri=MOCK_PROVIDER_URI,
gas_strategy=free_gas_price_strategy)
CURRENT_BLOCK = MOCK_TESTERCHAIN.w3.eth.getBlock(block_identifier='latest')

27
tests/unit/conftest.py Normal file
View File

@ -0,0 +1,27 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from tests.mock.interfaces import MockEthereumClient
@pytest.fixture(scope='function')
def mock_ethereum_client(mocker):
web3_mock = mocker.Mock()
mock_client = MockEthereumClient(w3=web3_mock)
return mock_client

View File

@ -24,13 +24,6 @@ from web3.exceptions import TransactionNotFound, TimeExhausted
from tests.mock.interfaces import MockEthereumClient
@pytest.fixture(scope='function')
def mock_ethereum_client(mocker):
web3_mock = mocker.Mock()
mock_client = MockEthereumClient(w3=web3_mock)
return mock_client
@pytest.fixture()
def receipt():
block_number_of_my_tx = 42

View File

@ -0,0 +1,43 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from web3.gas_strategies import time_based
from nucypher.blockchain.eth.interfaces import BlockchainInterface
def test_get_gas_strategy():
# Testing Web3's bundled time-based gas strategies
bundled_gas_strategies = {'glacial': time_based.glacial_gas_price_strategy, # 24h
'slow': time_based.slow_gas_price_strategy, # 1h
'medium': time_based.medium_gas_price_strategy, # 5m
'fast': time_based.fast_gas_price_strategy # 60s
}
for gas_strategy_name, expected_gas_strategy in bundled_gas_strategies.items():
gas_strategy = BlockchainInterface.get_gas_strategy(gas_strategy_name)
assert expected_gas_strategy == gas_strategy
assert callable(gas_strategy)
# Passing a callable gas strategy
callable_gas_strategy = time_based.glacial_gas_price_strategy
assert callable_gas_strategy == BlockchainInterface.get_gas_strategy(callable_gas_strategy)
# Passing None should retrieve the default gas strategy
assert BlockchainInterface.DEFAULT_GAS_STRATEGY == 'fast'
default = bundled_gas_strategies[BlockchainInterface.DEFAULT_GAS_STRATEGY]
gas_strategy = BlockchainInterface.get_gas_strategy()
assert default == gas_strategy

View File

@ -0,0 +1,148 @@
"""
This file is part of nucypher.
nucypher is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
nucypher is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
"""
from unittest.mock import patch
import pytest
from requests.exceptions import ConnectionError
from web3 import Web3
from nucypher.utilities.datafeeds import (
EtherchainGasPriceDatafeed,
Datafeed,
datafeed_fallback_gas_price_strategy,
UpvestGasPriceDatafeed
)
etherchain_json = {
"safeLow": "99.0",
"standard": "105.0",
"fast": "108.0",
"fastest": "119.9"
}
upvest_json = {
"success": True,
"updated": "2020-08-19T02:38:00.172Z",
"estimates": {
"fastest": 105.2745,
"fast": 97.158,
"medium": 91.424,
"slow": 87.19
}
}
def test_probe_datafeed(mocker):
feed = Datafeed()
feed.api_url = "http://foo.bar"
with patch('requests.get', side_effect=ConnectionError("Bad connection")) as mocked_get:
with pytest.raises(Datafeed.DatafeedError, match="Bad connection"):
feed._probe_feed()
mocked_get.assert_called_once_with(feed.api_url)
bad_response = mocker.Mock()
bad_response.status_code = 400
with patch('requests.get') as mocked_get:
mocked_get.return_value = bad_response
with pytest.raises(Datafeed.DatafeedError, match="status code 400"):
feed._probe_feed()
mocked_get.assert_called_once_with(feed.api_url)
json = {'foo': 'bar'}
good_response = mocker.Mock()
good_response.status_code = 200
good_response.json = mocker.Mock(return_value=json)
with patch('requests.get') as mocked_get:
mocked_get.return_value = good_response
feed._probe_feed()
mocked_get.assert_called_once_with(feed.api_url)
assert feed._raw_data == json
def test_etherchain():
feed = EtherchainGasPriceDatafeed()
assert set(feed._speed_names).issubset(etherchain_json.keys())
assert feed._default_speed in etherchain_json.keys()
with patch.object(feed, '_probe_feed'):
feed._raw_data = etherchain_json
assert feed.get_gas_price('safeLow') == Web3.toWei(99.0, 'gwei')
assert feed.get_gas_price('standard') == Web3.toWei(105.0, 'gwei')
assert feed.get_gas_price('fast') == Web3.toWei(108.0, 'gwei')
assert feed.get_gas_price('fastest') == Web3.toWei(119.9, 'gwei')
assert feed.get_gas_price() == feed.get_gas_price('fast') # Default
parsed_gas_prices = feed.gas_prices
with patch('nucypher.utilities.datafeeds.EtherchainGasPriceDatafeed._parse_gas_prices', autospec=True):
EtherchainGasPriceDatafeed.gas_prices = dict()
with patch.dict(EtherchainGasPriceDatafeed.gas_prices, values=parsed_gas_prices):
gas_strategy = feed.construct_gas_strategy()
assert gas_strategy("web3", "tx") == Web3.toWei(108.0, 'gwei')
def test_upvest():
feed = UpvestGasPriceDatafeed()
assert set(feed._speed_names).issubset(upvest_json['estimates'].keys())
assert feed._default_speed in upvest_json['estimates'].keys()
with patch.object(feed, '_probe_feed'):
feed._raw_data = upvest_json
assert feed.get_gas_price('slow') == Web3.toWei(87.19, 'gwei')
assert feed.get_gas_price('medium') == Web3.toWei(91.424, 'gwei')
assert feed.get_gas_price('fast') == Web3.toWei(97.158, 'gwei')
assert feed.get_gas_price('fastest') == Web3.toWei(105.2745, 'gwei')
assert feed.get_gas_price() == feed.get_gas_price('fastest') # Default
parsed_gas_prices = feed.gas_prices
with patch('nucypher.utilities.datafeeds.UpvestGasPriceDatafeed._parse_gas_prices', autospec=True):
UpvestGasPriceDatafeed.gas_prices = dict()
with patch.dict(UpvestGasPriceDatafeed.gas_prices, values=parsed_gas_prices):
gas_strategy = feed.construct_gas_strategy()
assert gas_strategy("web3", "tx") == Web3.toWei(105.2745, 'gwei')
def test_datafeed_fallback_gas_price_strategy():
mocked_gas_price = 0xFABADA
def mock_gas_strategy(web3, tx=None):
return mocked_gas_price
# In normal circumstances, the first datafeed (Etherchain) will return the gas price
with patch('nucypher.utilities.datafeeds.EtherchainGasPriceDatafeed.construct_gas_strategy',
return_value=mock_gas_strategy):
assert datafeed_fallback_gas_price_strategy("web3", "tx") == mocked_gas_price
# If the first datafeed in the chain fails, we resort to the second one
with patch('nucypher.utilities.datafeeds.EtherchainGasPriceDatafeed._probe_feed',
side_effect=Datafeed.DatafeedError):
with patch('nucypher.utilities.datafeeds.UpvestGasPriceDatafeed.construct_gas_strategy',
return_value=mock_gas_strategy):
assert datafeed_fallback_gas_price_strategy("web3", "tx") == mocked_gas_price
# If both datafeeds fail, we fallback to the rpc_gas_price_strategy
with patch('nucypher.utilities.datafeeds.EtherchainGasPriceDatafeed._probe_feed',
side_effect=Datafeed.DatafeedError):
with patch('nucypher.utilities.datafeeds.UpvestGasPriceDatafeed._probe_feed',
side_effect=Datafeed.DatafeedError):
with patch('nucypher.utilities.datafeeds.rpc_gas_price_strategy', side_effect=mock_gas_strategy):
assert datafeed_fallback_gas_price_strategy("web3", "tx") == mocked_gas_price

View File

@ -16,13 +16,18 @@
"""
import datetime
from unittest.mock import PropertyMock
import pytest
from web3 import HTTPProvider, IPCProvider, WebsocketProvider
from nucypher.blockchain.eth.clients import (EthereumClient, GanacheClient, GethClient, InfuraClient, PUBLIC_CHAINS,
ParityClient)
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from tests.mock.interfaces import MockEthereumClient
DEFAULT_GAS_PRICE = 42
GAS_PRICE_FROM_STRATEGY = 1234
#
# Mock Providers
@ -330,3 +335,14 @@ def test_ganache_web3_client():
assert interface.client.platform is None
assert interface.client.backend == 'ethereum-js'
assert interface.client.is_local
def test_gas_prices(mocker, mock_ethereum_client):
web3_mock = mock_ethereum_client.w3
web3_mock.eth.generateGasPrice = mocker.Mock(side_effect=[None, GAS_PRICE_FROM_STRATEGY])
type(web3_mock.eth).gasPrice = PropertyMock(return_value=DEFAULT_GAS_PRICE) # See docs of PropertyMock
assert mock_ethereum_client.gas_price == DEFAULT_GAS_PRICE
assert mock_ethereum_client.gas_price_for_transaction("there's no gas strategy") == DEFAULT_GAS_PRICE
assert mock_ethereum_client.gas_price_for_transaction("2nd time is the charm") == GAS_PRICE_FROM_STRATEGY

View File

@ -77,6 +77,7 @@ class TesterBlockchain(BlockchainDeployerInterface):
GAS_STRATEGIES = {**BlockchainDeployerInterface.GAS_STRATEGIES,
'free': free_gas_price_strategy}
DEFAULT_GAS_STRATEGY = 'free'
_PROVIDER_URI = 'tester://pyevm'
_compiler = SolidityCompiler(source_dirs=[(SolidityCompiler.default_contract_dir(), {TEST_CONTRACTS_DIR})])