nucypher/tests/unit/test_web3_clients.py

336 lines
9.6 KiB
Python

import datetime
from unittest.mock import Mock, PropertyMock
import pytest
from web3 import HTTPProvider, IPCProvider, WebsocketProvider
from nucypher.blockchain.eth.clients import (
AlchemyClient,
GanacheClient,
GethClient,
InfuraClient,
ParityClient,
)
from nucypher.blockchain.eth.interfaces import BlockchainInterface
from nucypher.utilities.networking import LOOPBACK_ADDRESS
DEFAULT_GAS_PRICE = 42
GAS_PRICE_FROM_STRATEGY = 1234
#
# Mock Providers
#
class MockGethProvider:
endpoint_uri = 'file:///ipc.geth'
client_version = 'Geth/v1.4.11-stable-fed692f6/darwin/go1.7'
class MockParityProvider:
endpoint_uri = 'file:///ipc.parity'
client_version = 'Parity-Ethereum/v2.5.1-beta-e0141f8-20190510/x86_64-linux-gnu/rustc1.34.1'
class MockGanacheProvider:
endpoint_uri = 'http://ganache:8445'
client_version = 'EthereumJS TestRPC/v2.1.5/ethereum-js'
class MockInfuraProvider:
endpoint_uri = "wss://:@sepolia.infura.io/ws/v3/1234567890987654321abcdef"
client_version = "Geth/v1.8.23-omnibus-2ad89aaa/linux-amd64/go1.11.1"
class MockAlchemyProvider:
endpoint_uri = 'https://eth-rinkeby.alchemyapi.io/v2/1234567890987654321abcdef'
client_version = 'Geth/v1.9.20-stable-979fc968/linux-amd64/go1.15'
class MockWebSocketProvider:
endpoint_uri = f'ws://{LOOPBACK_ADDRESS}:8546'
client_version = 'Geth/v1.8.23-omnibus-2ad89aaa/linux-amd64/go1.11.1'
class SyncedMockW3Eth:
# Support older and newer versions of web3 py in-test
version = 5
chain_id = hex(5)
block_number = 5
def getBlock(self, blockNumber):
return {
'timestamp': datetime.datetime.timestamp(datetime.datetime.now() - datetime.timedelta(seconds=25))
}
class MockedW3GethWithPeers:
@property
def admin(self):
class GethAdmin:
def peers(self):
return [1, 2, 3]
return GethAdmin()
class MockedW3GethWithNoPeers:
@property
def admin(self):
class GethAdmin:
def peers(self):
return []
return GethAdmin()
#
# Mock Web3
#
class SyncedMockWeb3:
net = SyncedMockW3Eth()
eth = SyncedMockW3Eth()
geth = MockedW3GethWithPeers()
def __init__(self, provider):
self.provider = provider
self.middleware_onion = Mock()
@property
def client_version(self):
return self.provider.client_version
@property
def is_connected(self):
return lambda: True
#
# Mock Blockchain
#
class BlockchainInterfaceTestBase(BlockchainInterface):
Web3 = SyncedMockWeb3
def _configure_registry(self, *args, **kwargs):
pass
def _setup_solidity(self, *args, **kwargs):
pass
def attach_middleware(self):
pass
class ProviderTypeTestClient(BlockchainInterfaceTestBase):
def __init__(self,
expected_provider_class,
actual_provider_to_attach,
*args,
**kwargs):
super().__init__(*args, **kwargs)
self.expected_provider_class = expected_provider_class
self.test_provider_to_attach = actual_provider_to_attach
def _attach_blockchain_provider(self, *args, **kwargs) -> None:
super()._attach_blockchain_provider(*args, **kwargs)
# check type
assert isinstance(self.provider, self.expected_provider_class)
super()._attach_blockchain_provider(provider=self.test_provider_to_attach)
class InfuraTestClient(BlockchainInterfaceTestBase):
def _attach_blockchain_provider(self, *args, **kwargs) -> None:
super()._attach_blockchain_provider(provider=MockInfuraProvider())
class AlchemyTestClient(BlockchainInterfaceTestBase):
def _attach_blockchain_provider(self, *args, **kwargs) -> None:
super()._attach_blockchain_provider(provider=MockAlchemyProvider())
class GethClientTestBlockchain(BlockchainInterfaceTestBase):
def _attach_blockchain_provider(self, *args, **kwargs) -> None:
super()._attach_blockchain_provider(provider=MockGethProvider())
class ParityClientTestInterface(BlockchainInterfaceTestBase):
def _attach_blockchain_provider(self, *args, **kwargs) -> None:
super()._attach_blockchain_provider(provider=MockParityProvider())
class GanacheClientTestInterface(BlockchainInterfaceTestBase):
def _attach_blockchain_provider(self, *args, **kwargs) -> None:
super()._attach_blockchain_provider(provider=MockGanacheProvider())
def test_client_no_provider():
with pytest.raises(BlockchainInterface.NoProvider):
interface = BlockchainInterfaceTestBase()
interface.connect()
def test_geth_web3_client():
interface = GethClientTestBlockchain(endpoint="file:///ipc.geth")
interface.connect()
assert isinstance(interface.client, GethClient)
assert interface.client.node_technology == 'Geth'
assert interface.client.node_version == 'v1.4.11-stable-fed692f6'
assert interface.client.platform == 'darwin'
assert interface.client.backend == 'go1.7'
assert interface.client.is_local is False
assert interface.client.chain_id == 5 # Hardcoded above
def test_autodetect_provider_type_file(tempfile_path):
interface = ProviderTypeTestClient(
endpoint=str(tempfile_path), # existing file for test
expected_provider_class=IPCProvider,
actual_provider_to_attach=MockGethProvider(),
)
interface.connect()
assert isinstance(interface.client, GethClient)
def test_autodetect_provider_type_file_none_existent():
with pytest.raises(BlockchainInterface.UnsupportedProvider):
interface = BlockchainInterfaceTestBase(endpoint="/none_existent.ipc.geth")
interface.connect()
def test_detect_provider_type_file():
interface = ProviderTypeTestClient(
endpoint="file:///ipc.geth",
expected_provider_class=IPCProvider,
actual_provider_to_attach=MockGethProvider(),
)
interface.connect()
assert isinstance(interface.client, GethClient)
def test_detect_provider_type_ipc():
interface = ProviderTypeTestClient(
endpoint="ipc:///ipc.geth",
expected_provider_class=IPCProvider,
actual_provider_to_attach=MockGethProvider(),
)
interface.connect()
assert isinstance(interface.client, GethClient)
def test_detect_provider_type_http():
interface = ProviderTypeTestClient(
endpoint="http://ganache:8445",
expected_provider_class=HTTPProvider,
actual_provider_to_attach=MockGanacheProvider(),
)
interface.connect()
assert isinstance(interface.client, GanacheClient)
def test_detect_provider_type_https():
interface = ProviderTypeTestClient(
endpoint="https://ganache:8445",
expected_provider_class=HTTPProvider,
actual_provider_to_attach=MockGanacheProvider(),
)
interface.connect()
assert isinstance(interface.client, GanacheClient)
def test_detect_provider_type_ws():
interface = ProviderTypeTestClient(
endpoint=f"ws://{LOOPBACK_ADDRESS}:8546",
expected_provider_class=WebsocketProvider,
actual_provider_to_attach=MockWebSocketProvider(),
)
interface.connect()
assert isinstance(interface.client, GethClient)
def test_infura_web3_client():
interface = InfuraTestClient(
endpoint="wss://:@goerli.infura.io/ws/v3/1234567890987654321abcdef"
)
interface.connect()
assert isinstance(interface.client, InfuraClient)
assert interface.client.node_technology == 'Geth'
assert interface.client.node_version == 'v1.8.23-omnibus-2ad89aaa'
assert interface.client.platform == 'linux-amd64'
assert interface.client.backend == 'go1.11.1'
assert interface.client.is_local is False
assert interface.client.chain_id == 5
assert interface.client.unlock_account('address', 'password') # Returns True on success
def test_alchemy_web3_client():
interface = AlchemyTestClient(
endpoint="https://eth-rinkeby.alchemyapi.io/v2/1234567890987654321abcdef"
)
interface.connect()
assert isinstance(interface.client, AlchemyClient)
assert interface.client.node_technology == 'Geth'
assert interface.client.node_version == 'v1.9.20-stable-979fc968'
assert interface.client.platform == 'linux-amd64'
assert interface.client.backend == 'go1.15'
def test_parity_web3_client():
interface = ParityClientTestInterface(endpoint="file:///ipc.parity")
interface.connect()
assert isinstance(interface.client, ParityClient)
assert interface.client.node_technology == 'Parity-Ethereum'
assert interface.client.node_version == 'v2.5.1-beta-e0141f8-20190510'
assert interface.client.platform == 'x86_64-linux-gnu'
assert interface.client.backend == 'rustc1.34.1'
def test_ganache_web3_client():
interface = GanacheClientTestInterface(endpoint="http://ganache:8445")
interface.connect()
assert isinstance(interface.client, GanacheClient)
assert interface.client.node_technology == 'EthereumJS TestRPC'
assert interface.client.node_version == 'v2.1.5'
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.generate_gas_price = mocker.Mock(side_effect=[None, GAS_PRICE_FROM_STRATEGY])
type(web3_mock.eth).gas_price = 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