mirror of https://github.com/nucypher/nucypher.git
259 lines
11 KiB
Python
259 lines
11 KiB
Python
|
|
|
|
|
|
from collections.abc import Callable
|
|
from statistics import median
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from constant_sorrow.constants import SLOW, MEDIUM, FAST, FASTEST
|
|
from requests.exceptions import ConnectionError
|
|
from web3 import Web3
|
|
|
|
from nucypher.utilities.datafeeds import (
|
|
Datafeed,
|
|
EtherchainGasPriceDatafeed,
|
|
EthereumGasPriceDatafeed,
|
|
UpvestGasPriceDatafeed,
|
|
ZoltuGasPriceDatafeed
|
|
)
|
|
from nucypher.utilities.gas_strategies import construct_datafeed_median_strategy
|
|
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
zoltu_json = {
|
|
"number_of_blocks": 200,
|
|
"latest_block_number": 11294588,
|
|
"percentile_1": "1 nanoeth",
|
|
"percentile_2": "1 nanoeth",
|
|
"percentile_3": "3.452271231 nanoeth",
|
|
"percentile_4": "10 nanoeth",
|
|
"percentile_5": "10 nanoeth",
|
|
"percentile_10": "18.15 nanoeth",
|
|
"percentile_15": "25 nanoeth",
|
|
"percentile_20": "28 nanoeth",
|
|
"percentile_25": "30 nanoeth",
|
|
"percentile_30": "32 nanoeth",
|
|
"percentile_35": "37 nanoeth",
|
|
"percentile_40": "41 nanoeth",
|
|
"percentile_45": "44 nanoeth",
|
|
"percentile_50": "47.000001459 nanoeth",
|
|
"percentile_55": "50 nanoeth",
|
|
"percentile_60": "52.5 nanoeth",
|
|
"percentile_65": "55.20000175 nanoeth",
|
|
"percentile_70": "56.1 nanoeth",
|
|
"percentile_75": "58 nanoeth",
|
|
"percentile_80": "60.20000175 nanoeth",
|
|
"percentile_85": "63 nanoeth",
|
|
"percentile_90": "64 nanoeth",
|
|
"percentile_95": "67 nanoeth",
|
|
"percentile_96": "67.32 nanoeth",
|
|
"percentile_97": "68 nanoeth",
|
|
"percentile_98": "70 nanoeth",
|
|
"percentile_99": "71 nanoeth",
|
|
"percentile_100": "74 nanoeth"
|
|
}
|
|
|
|
|
|
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_canonical_speed_names():
|
|
# Valid speed names, grouped in equivalence classes
|
|
speed_equivalence_classes = {
|
|
SLOW: ('slow', 'SLOW', 'Slow',
|
|
'safeLow', 'safelow', 'SafeLow', 'SAFELOW',
|
|
'low', 'LOW', 'Low'),
|
|
MEDIUM: ('medium', 'MEDIUM', 'Medium',
|
|
'standard', 'STANDARD', 'Standard',
|
|
'average', 'AVERAGE', 'Average'),
|
|
FAST: ('fast', 'FAST', 'Fast',
|
|
'high', 'HIGH', 'High'),
|
|
FASTEST: ('fastest', 'FASTEST', 'Fastest')
|
|
}
|
|
|
|
for canonical_speed, equivalence_class in speed_equivalence_classes.items():
|
|
for speed_name in equivalence_class:
|
|
assert canonical_speed == EthereumGasPriceDatafeed.get_canonical_speed(speed_name)
|
|
|
|
# Invalid speed names, but that are somewhat similar to a valid one so we can give a suggestion
|
|
similarities = (
|
|
('hihg', 'high'),
|
|
('zlow', 'low'),
|
|
)
|
|
|
|
for wrong_name, suggestion in similarities:
|
|
message = f"'{wrong_name}' is not a valid speed name. Did you mean '{suggestion}'?"
|
|
with pytest.raises(LookupError, match=message):
|
|
EthereumGasPriceDatafeed.get_canonical_speed(wrong_name)
|
|
|
|
# Utterly wrong speed names. Shame on you.
|
|
wrong_name = "🙈"
|
|
with pytest.raises(LookupError, match=f"'{wrong_name}' is not a valid speed name."):
|
|
EthereumGasPriceDatafeed.get_canonical_speed(wrong_name)
|
|
|
|
|
|
def test_etherchain():
|
|
feed = EtherchainGasPriceDatafeed()
|
|
|
|
assert set(feed._speed_names.values()).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.to_wei(99.0, 'gwei')
|
|
assert feed.get_gas_price('standard') == Web3.to_wei(105.0, 'gwei')
|
|
assert feed.get_gas_price('fast') == Web3.to_wei(108.0, 'gwei')
|
|
assert feed.get_gas_price('fastest') == Web3.to_wei(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.to_wei(108.0, 'gwei')
|
|
|
|
|
|
def test_upvest():
|
|
feed = UpvestGasPriceDatafeed()
|
|
|
|
assert set(feed._speed_names.values()).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.to_wei(87.19, 'gwei')
|
|
assert feed.get_gas_price('medium') == Web3.to_wei(91.424, 'gwei')
|
|
assert feed.get_gas_price('fast') == Web3.to_wei(97.158, 'gwei')
|
|
assert feed.get_gas_price('fastest') == Web3.to_wei(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.to_wei(105.2745, 'gwei')
|
|
|
|
|
|
def test_zoltu():
|
|
feed = ZoltuGasPriceDatafeed()
|
|
|
|
assert set(feed._speed_names.values()).issubset(zoltu_json.keys())
|
|
# assert feed._default_speed in zoltu_json.keys()
|
|
|
|
with patch.object(feed, '_probe_feed'):
|
|
feed._raw_data = zoltu_json
|
|
assert feed.get_gas_price('slow') == Web3.to_wei(41, 'gwei')
|
|
assert feed.get_gas_price('medium') == Web3.to_wei(58, 'gwei')
|
|
assert feed.get_gas_price('fast') == Web3.to_wei(67, 'gwei')
|
|
assert feed.get_gas_price('fastest') == Web3.to_wei(70, 'gwei')
|
|
assert feed.get_gas_price() == feed.get_gas_price('fast') # Default
|
|
parsed_gas_prices = feed.gas_prices
|
|
|
|
with patch('nucypher.utilities.datafeeds.ZoltuGasPriceDatafeed._parse_gas_prices', autospec=True):
|
|
ZoltuGasPriceDatafeed.gas_prices = dict()
|
|
with patch.dict(ZoltuGasPriceDatafeed.gas_prices, values=parsed_gas_prices):
|
|
gas_strategy = feed.construct_gas_strategy()
|
|
assert gas_strategy("web3", "tx") == Web3.to_wei(67, 'gwei')
|
|
|
|
|
|
def test_datafeed_median_gas_price_strategy():
|
|
|
|
mock_etherchain_gas_price = 1000
|
|
mock_upvest_gas_price = 2000
|
|
mock_zoltu_gas_price = 4000
|
|
mock_rpc_gas_price = 42
|
|
|
|
def construct_mock_gas_strategy(gas_price) -> Callable:
|
|
def _mock_gas_strategy(web3, tx=None):
|
|
return gas_price
|
|
return _mock_gas_strategy
|
|
|
|
# In normal circumstances, all datafeeds in the strategy work, and the median is returned
|
|
with patch('nucypher.utilities.datafeeds.UpvestGasPriceDatafeed.construct_gas_strategy',
|
|
return_value=construct_mock_gas_strategy(mock_upvest_gas_price)):
|
|
# with patch('nucypher.utilities.datafeeds.EtherchainGasPriceDatafeed.construct_gas_strategy',
|
|
# return_value=construct_mock_gas_strategy(mock_etherchain_gas_price)):
|
|
with patch('nucypher.utilities.datafeeds.ZoltuGasPriceDatafeed.construct_gas_strategy',
|
|
return_value=construct_mock_gas_strategy(mock_zoltu_gas_price)):
|
|
datafeed_median_gas_price_strategy = construct_datafeed_median_strategy()
|
|
assert datafeed_median_gas_price_strategy("web3", "tx") == median([mock_upvest_gas_price,
|
|
mock_zoltu_gas_price])
|
|
|
|
# If, for example, Upvest fails, the median is computed using the other two feeds
|
|
with patch('nucypher.utilities.datafeeds.UpvestGasPriceDatafeed._probe_feed',
|
|
side_effect=Datafeed.DatafeedError):
|
|
# with patch('nucypher.utilities.datafeeds.EtherchainGasPriceDatafeed.construct_gas_strategy',
|
|
# return_value=construct_mock_gas_strategy(mock_etherchain_gas_price)):
|
|
with patch('nucypher.utilities.datafeeds.ZoltuGasPriceDatafeed.construct_gas_strategy',
|
|
return_value=construct_mock_gas_strategy(mock_zoltu_gas_price)):
|
|
datafeed_median_gas_price_strategy = construct_datafeed_median_strategy()
|
|
assert datafeed_median_gas_price_strategy("web3", "tx") == median([mock_zoltu_gas_price])
|
|
|
|
# If only one feed works, then the return value corresponds to this feed
|
|
with patch('nucypher.utilities.datafeeds.UpvestGasPriceDatafeed._probe_feed',
|
|
side_effect=Datafeed.DatafeedError):
|
|
# with patch('nucypher.utilities.datafeeds.EtherchainGasPriceDatafeed._probe_feed',
|
|
# side_effect=Datafeed.DatafeedError):
|
|
with patch('nucypher.utilities.datafeeds.ZoltuGasPriceDatafeed.construct_gas_strategy',
|
|
return_value=construct_mock_gas_strategy(mock_zoltu_gas_price)):
|
|
datafeed_median_gas_price_strategy = construct_datafeed_median_strategy()
|
|
assert datafeed_median_gas_price_strategy("web3", "tx") == mock_zoltu_gas_price
|
|
|
|
# If all feeds 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.ZoltuGasPriceDatafeed._probe_feed',
|
|
side_effect=Datafeed.DatafeedError):
|
|
with patch('nucypher.utilities.gas_strategies.rpc_gas_price_strategy',
|
|
side_effect=construct_mock_gas_strategy(mock_rpc_gas_price)):
|
|
datafeed_median_gas_price_strategy = construct_datafeed_median_strategy()
|
|
assert datafeed_median_gas_price_strategy("web3", "tx") == mock_rpc_gas_price
|