mirror of https://github.com/nucypher/nucypher.git
Renaming Oracles to Datafeeds
parent
7d5646e2f5
commit
066fd8461d
|
@ -57,7 +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.oracles import oracle_fallback_gas_price_strategy
|
||||
from nucypher.utilities.datafeeds import datafeed_fallback_gas_price_strategy
|
||||
|
||||
Web3Providers = Union[IPCProvider, WebsocketProvider, HTTPProvider, EthereumTester]
|
||||
|
||||
|
@ -281,7 +281,7 @@ class BlockchainInterface:
|
|||
# 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 = oracle_fallback_gas_price_strategy
|
||||
gas_strategy = datafeed_fallback_gas_price_strategy
|
||||
else:
|
||||
gas_strategy = self.gas_strategy
|
||||
self.client.set_gas_strategy(gas_strategy=gas_strategy)
|
||||
|
|
|
@ -23,24 +23,24 @@ from web3.gas_strategies.rpc import rpc_gas_price_strategy
|
|||
from web3.types import Wei, TxParams
|
||||
|
||||
|
||||
class Oracle(ABC):
|
||||
class Datafeed(ABC):
|
||||
|
||||
class OracleError(RuntimeError):
|
||||
"""Base class for Oracle-related exceptions"""
|
||||
class DatafeedError(RuntimeError):
|
||||
"""Base class for exceptions concerning Datafeeds"""
|
||||
|
||||
name = NotImplemented
|
||||
api_url = NotImplemented # TODO: Deal with API keys
|
||||
|
||||
def _probe_oracle(self):
|
||||
def _probe_feed(self):
|
||||
try:
|
||||
response = requests.get(self.api_url)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
error = f"Failed to probe oracle at {self.api_url}: {str(e)}"
|
||||
raise self.OracleError(error)
|
||||
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 oracle at {self.api_url} with status code {response.status_code}"
|
||||
raise self.OracleError(error)
|
||||
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()
|
||||
|
||||
|
@ -48,8 +48,8 @@ class Oracle(ABC):
|
|||
return f"{self.name} ({self.api_url})"
|
||||
|
||||
|
||||
class EthereumGasPriceOracle(Oracle):
|
||||
"""Base class for Ethereum gas price oracles"""
|
||||
class EthereumGasPriceDatafeed(Datafeed):
|
||||
"""Base class for Ethereum gas price data feeds"""
|
||||
|
||||
_speed_names = NotImplemented
|
||||
_default_speed = NotImplemented
|
||||
|
@ -66,47 +66,47 @@ class EthereumGasPriceOracle(Oracle):
|
|||
|
||||
@classmethod
|
||||
def construct_gas_strategy(cls):
|
||||
def oracle_based_gas_price_strategy(web3: Web3, transaction_params: TxParams = None) -> Wei:
|
||||
oracle = cls()
|
||||
gas_price = oracle.get_gas_price()
|
||||
def gas_price_strategy(web3: Web3, transaction_params: TxParams = None) -> Wei:
|
||||
feed = cls()
|
||||
gas_price = feed.get_gas_price()
|
||||
return gas_price
|
||||
return oracle_based_gas_price_strategy
|
||||
return gas_price_strategy
|
||||
|
||||
|
||||
class EtherchainGasPriceOracle(EthereumGasPriceOracle):
|
||||
"""Gas price oracle from Etherchain"""
|
||||
class EtherchainGasPriceDatafeed(EthereumGasPriceDatafeed):
|
||||
"""Gas price datafeed from Etherchain"""
|
||||
|
||||
name = "Etherchain oracle"
|
||||
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_oracle()
|
||||
self._probe_feed()
|
||||
self.gas_prices = {k: int(Web3.toWei(v, 'gwei')) for k, v in self._raw_data.items()}
|
||||
|
||||
|
||||
class UpvestGasPriceOracle(EthereumGasPriceOracle):
|
||||
"""Gas price oracle from Upvest"""
|
||||
class UpvestGasPriceDatafeed(EthereumGasPriceDatafeed):
|
||||
"""Gas price datafeed from Upvest"""
|
||||
|
||||
name = "Upvest oracle"
|
||||
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_oracle()
|
||||
self._probe_feed()
|
||||
self.gas_prices = {k: int(Web3.toWei(v, 'gwei')) for k, v in self._raw_data['estimates'].items()}
|
||||
|
||||
|
||||
def oracle_fallback_gas_price_strategy(web3: Web3, transaction_params: TxParams = None) -> Wei:
|
||||
oracles = (EtherchainGasPriceOracle, UpvestGasPriceOracle)
|
||||
def datafeed_fallback_gas_price_strategy(web3: Web3, transaction_params: TxParams = None) -> Wei:
|
||||
feeds = (EtherchainGasPriceDatafeed, UpvestGasPriceDatafeed)
|
||||
|
||||
for gas_price_oracle_class in oracles:
|
||||
for gas_price_feed_class in feeds:
|
||||
try:
|
||||
gas_strategy = gas_price_oracle_class.construct_gas_strategy()
|
||||
gas_strategy = gas_price_feed_class.construct_gas_strategy()
|
||||
gas_price = gas_strategy(web3, transaction_params)
|
||||
except Oracle.OracleError:
|
||||
except Datafeed.DatafeedError:
|
||||
continue
|
||||
else:
|
||||
return gas_price
|
||||
|
@ -116,5 +116,5 @@ def oracle_fallback_gas_price_strategy(web3: Web3, transaction_params: TxParams
|
|||
|
||||
|
||||
|
||||
# TODO: We can implement here other oracles, like the ETH/USD (e.g., https://api.coinmarketcap.com/v1/ticker/ethereum/)
|
||||
# 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
|
|
@ -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
|
|
@ -1,145 +0,0 @@
|
|||
"""
|
||||
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.oracles import (
|
||||
EtherchainGasPriceOracle,
|
||||
Oracle,
|
||||
oracle_fallback_gas_price_strategy,
|
||||
UpvestGasPriceOracle
|
||||
)
|
||||
|
||||
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_oracle(mocker):
|
||||
|
||||
oracle = Oracle()
|
||||
oracle.api_url = "http://foo.bar"
|
||||
|
||||
with patch('requests.get', side_effect=ConnectionError("Bad connection")) as mocked_get:
|
||||
with pytest.raises(Oracle.OracleError, match="Bad connection"):
|
||||
oracle._probe_oracle()
|
||||
mocked_get.assert_called_once_with(oracle.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(Oracle.OracleError, match="status code 400"):
|
||||
oracle._probe_oracle()
|
||||
mocked_get.assert_called_once_with(oracle.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
|
||||
oracle._probe_oracle()
|
||||
mocked_get.assert_called_once_with(oracle.api_url)
|
||||
assert oracle._raw_data == json
|
||||
|
||||
|
||||
def test_etherchain():
|
||||
oracle = EtherchainGasPriceOracle()
|
||||
|
||||
assert set(oracle._speed_names).issubset(etherchain_json.keys())
|
||||
assert oracle._default_speed in etherchain_json.keys()
|
||||
|
||||
with patch.object(oracle, '_probe_oracle'):
|
||||
oracle._raw_data = etherchain_json
|
||||
assert oracle.get_gas_price('safeLow') == Web3.toWei(99.0, 'gwei')
|
||||
assert oracle.get_gas_price('standard') == Web3.toWei(105.0, 'gwei')
|
||||
assert oracle.get_gas_price('fast') == Web3.toWei(108.0, 'gwei')
|
||||
assert oracle.get_gas_price('fastest') == Web3.toWei(119.9, 'gwei')
|
||||
assert oracle.get_gas_price() == oracle.get_gas_price('fast') # Default
|
||||
parsed_gas_prices = oracle.gas_prices
|
||||
|
||||
with patch('nucypher.utilities.oracles.EtherchainGasPriceOracle._parse_gas_prices', autospec=True):
|
||||
EtherchainGasPriceOracle.gas_prices = dict()
|
||||
with patch.dict(EtherchainGasPriceOracle.gas_prices, values=parsed_gas_prices):
|
||||
gas_strategy = oracle.construct_gas_strategy()
|
||||
assert gas_strategy("web3", "tx") == Web3.toWei(108.0, 'gwei')
|
||||
|
||||
|
||||
def test_upvest():
|
||||
oracle = UpvestGasPriceOracle()
|
||||
|
||||
assert set(oracle._speed_names).issubset(upvest_json['estimates'].keys())
|
||||
assert oracle._default_speed in upvest_json['estimates'].keys()
|
||||
|
||||
with patch.object(oracle, '_probe_oracle'):
|
||||
oracle._raw_data = upvest_json
|
||||
assert oracle.get_gas_price('slow') == Web3.toWei(87.19, 'gwei')
|
||||
assert oracle.get_gas_price('medium') == Web3.toWei(91.424, 'gwei')
|
||||
assert oracle.get_gas_price('fast') == Web3.toWei(97.158, 'gwei')
|
||||
assert oracle.get_gas_price('fastest') == Web3.toWei(105.2745, 'gwei')
|
||||
assert oracle.get_gas_price() == oracle.get_gas_price('fastest') # Default
|
||||
parsed_gas_prices = oracle.gas_prices
|
||||
|
||||
with patch('nucypher.utilities.oracles.UpvestGasPriceOracle._parse_gas_prices', autospec=True):
|
||||
UpvestGasPriceOracle.gas_prices = dict()
|
||||
with patch.dict(UpvestGasPriceOracle.gas_prices, values=parsed_gas_prices):
|
||||
gas_strategy = oracle.construct_gas_strategy()
|
||||
assert gas_strategy("web3", "tx") == Web3.toWei(105.2745, 'gwei')
|
||||
|
||||
|
||||
def test_oracle_fallback_gas_price_strategy():
|
||||
|
||||
mocked_gas_price = 0xFABADA
|
||||
|
||||
def mock_gas_strategy(web3, tx=None):
|
||||
return mocked_gas_price
|
||||
|
||||
# In normal circumstances, the first oracle (Etherchain) will return the gas price
|
||||
with patch('nucypher.utilities.oracles.EtherchainGasPriceOracle.construct_gas_strategy',
|
||||
return_value=mock_gas_strategy):
|
||||
assert oracle_fallback_gas_price_strategy("web3", "tx") == mocked_gas_price
|
||||
|
||||
# If the first oracle in the chain fails, we resort to the second one
|
||||
with patch('nucypher.utilities.oracles.EtherchainGasPriceOracle._probe_oracle', side_effect=Oracle.OracleError):
|
||||
with patch('nucypher.utilities.oracles.UpvestGasPriceOracle.construct_gas_strategy',
|
||||
return_value=mock_gas_strategy):
|
||||
assert oracle_fallback_gas_price_strategy("web3", "tx") == mocked_gas_price
|
||||
|
||||
# If both oracles fail, we fallback to the rpc_gas_price_strategy
|
||||
with patch('nucypher.utilities.oracles.EtherchainGasPriceOracle._probe_oracle', side_effect=Oracle.OracleError):
|
||||
with patch('nucypher.utilities.oracles.UpvestGasPriceOracle._probe_oracle', side_effect=Oracle.OracleError):
|
||||
with patch('nucypher.utilities.oracles.rpc_gas_price_strategy', side_effect=mock_gas_strategy):
|
||||
assert oracle_fallback_gas_price_strategy("web3", "tx") == mocked_gas_price
|
Loading…
Reference in New Issue