mirror of https://github.com/nucypher/nucypher.git
Merge pull request #2154 from cygnusv/mamma-mia
Improved gas price logic. Introduction of gas oraclespull/2198/head
commit
60ebeacd64
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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})])
|
||||
|
|
Loading…
Reference in New Issue