mirror of https://github.com/nucypher/nucypher.git
First pass at integrating gas price oracles
parent
1fbd2514fe
commit
d6c986d9bb
|
@ -0,0 +1,97 @@
|
|||
"""
|
||||
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.types import Wei
|
||||
|
||||
|
||||
class Oracle(ABC):
|
||||
|
||||
class OracleError(RuntimeError):
|
||||
"""Base class for Oracle-related exceptions"""
|
||||
|
||||
api_url = NotImplemented # TODO: Deal with API keys
|
||||
|
||||
def _probe_oracle(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)
|
||||
|
||||
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)
|
||||
|
||||
self._raw_data = response.json()
|
||||
|
||||
|
||||
class EthereumGasPriceOracle(Oracle):
|
||||
"""Base class for Ethereum gas price oracles"""
|
||||
|
||||
_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 oracle_based_gas_price_strategy(web3, transaction_params=None) -> Wei:
|
||||
oracle = cls()
|
||||
gas_price = oracle.get_gas_price()
|
||||
return gas_price
|
||||
return oracle_based_gas_price_strategy
|
||||
|
||||
|
||||
class EtherchainGasPriceOracle(EthereumGasPriceOracle):
|
||||
"""Gas price oracle from Etherchain"""
|
||||
|
||||
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.gas_prices = {k: int(Web3.toWei(v, 'gwei')) for k, v in self._raw_data.items()}
|
||||
|
||||
|
||||
class UpvestGasPriceOracle(EthereumGasPriceOracle):
|
||||
"""Gas price oracle from Upvest"""
|
||||
|
||||
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.gas_prices = {k: int(Web3.toWei(v, 'gwei')) for k, v in self._raw_data['estimates'].items()}
|
||||
|
||||
|
||||
|
||||
# TODO: We can implement here other oracles, 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,91 @@
|
|||
"""
|
||||
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 Oracle, EtherchainGasPriceOracle, UpvestGasPriceOracle
|
||||
|
||||
|
||||
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():
|
||||
etherchain_json = {
|
||||
"safeLow": "99.0",
|
||||
"standard": "105.0",
|
||||
"fast": "108.0",
|
||||
"fastest": "119.9"
|
||||
}
|
||||
oracle = EtherchainGasPriceOracle()
|
||||
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
|
||||
|
||||
|
||||
def test_upvest():
|
||||
upvest_json = {
|
||||
"success": True,
|
||||
"updated": "2020-08-19T02:38:00.172Z",
|
||||
"estimates": {
|
||||
"fastest": 105.2745,
|
||||
"fast": 97.158,
|
||||
"medium": 91.424,
|
||||
"slow": 87.19
|
||||
}
|
||||
}
|
||||
oracle = UpvestGasPriceOracle()
|
||||
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
|
Loading…
Reference in New Issue