mirror of https://github.com/nucypher/nucypher.git
Merge pull request #3185 from KPrasch/multichain
Multichain Condition Evaluation and Fallbackpull/3203/head
commit
c8e1c9b46e
|
@ -0,0 +1,3 @@
|
|||
- Support arbitrary multichain configuration for EVM-compatible blockchains for condition evaluation by ursula.
|
||||
- Support for fallback RPC providers and multiple URI specification for a single chain ID.
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import time
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Tuple, Union
|
||||
from typing import DefaultDict, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
import maya
|
||||
from eth_typing import ChecksumAddress
|
||||
|
@ -22,7 +23,7 @@ from nucypher_core.ferveo import (
|
|||
Transcript,
|
||||
Validator,
|
||||
)
|
||||
from web3 import Web3
|
||||
from web3 import HTTPProvider, Web3
|
||||
from web3.types import TxReceipt
|
||||
|
||||
from nucypher.acumen.nicknames import Nickname
|
||||
|
@ -53,6 +54,7 @@ from nucypher.crypto.powers import (
|
|||
)
|
||||
from nucypher.datastore.dkg import DKGStorage
|
||||
from nucypher.network.trackers import OperatorBondedTracker
|
||||
from nucypher.policy.conditions.evm import _CONDITION_CHAINS
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo
|
||||
from nucypher.policy.payment import ContractPayment
|
||||
from nucypher.utilities.emitters import StdoutEmitter
|
||||
|
@ -175,9 +177,9 @@ class Operator(BaseActor):
|
|||
|
||||
# Falsy values may be passed down from the superclass
|
||||
if not eth_provider_uri:
|
||||
raise ValueError("ETH Provider URI is required to init a local character.")
|
||||
raise ValueError("ETH Provider URI is required to init an operator.")
|
||||
if not payment_method:
|
||||
raise ValueError("Payment method is required to init a local character.")
|
||||
raise ValueError("Payment method is required to init an operator.")
|
||||
|
||||
if not transacting_power:
|
||||
transacting_power = TransactingPower(
|
||||
|
@ -209,26 +211,6 @@ class Operator(BaseActor):
|
|||
)
|
||||
self.work_tracker = work_tracker or WorkTracker(worker=self)
|
||||
|
||||
#
|
||||
# Multi-provider support
|
||||
#
|
||||
|
||||
# TODO: Improve and formalize fully configurable multi-provider support
|
||||
# TODO: Abstract away payment provider #3004
|
||||
# TODO: #3094 Is chain ID stable and completely reliable?
|
||||
# TODO: Relocate to a higher layer
|
||||
eth_chain = self.application_agent.blockchain
|
||||
polygon_chain = self.payment_method.agent.blockchain
|
||||
|
||||
# TODO: Use clients layer?
|
||||
self.condition_providers = {
|
||||
eth_chain.client.chain_id: eth_chain.provider,
|
||||
polygon_chain.client.chain_id: polygon_chain.provider,
|
||||
}
|
||||
self.log.info(
|
||||
f"Connected to {len(self.condition_providers)} blockchains: {self.condition_providers}"
|
||||
)
|
||||
|
||||
def _local_operator_address(self):
|
||||
return self.__operator_address
|
||||
|
||||
|
@ -303,10 +285,11 @@ class Ritualist(BaseActor):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
provider_uri: str, # this is a blockchain connection to the chain with the coordinator contract
|
||||
coordinator_provider_uri: str,
|
||||
network: str, # this must be the network where the coordinator lives
|
||||
crypto_power: CryptoPower,
|
||||
transacting_power: TransactingPower,
|
||||
condition_provider_uris: Optional[Dict[int, List[str]]] = None,
|
||||
publish_finalization: bool = True, # TODO: Remove this
|
||||
*args,
|
||||
**kwargs,
|
||||
|
@ -318,7 +301,7 @@ class Ritualist(BaseActor):
|
|||
self.coordinator_agent = ContractAgency.get_agent(
|
||||
CoordinatorAgent,
|
||||
registry=InMemoryContractRegistry.from_latest_publication(network=network),
|
||||
provider_uri=provider_uri, # TODO: rename, this might be a polygon provider
|
||||
provider_uri=coordinator_provider_uri,
|
||||
)
|
||||
|
||||
# track active onchain rituals
|
||||
|
@ -338,6 +321,65 @@ class Ritualist(BaseActor):
|
|||
ThresholdRequestDecryptingPower
|
||||
) # used for secure decryption request channel
|
||||
|
||||
self.condition_providers = self.connect_condition_providers(
|
||||
condition_provider_uris
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_permitted_condition_chain(chain_id: int) -> bool:
|
||||
return int(chain_id) in [int(cid) for cid in _CONDITION_CHAINS.keys()]
|
||||
|
||||
@staticmethod
|
||||
def _make_condition_provider(uri: str) -> HTTPProvider:
|
||||
provider = HTTPProvider(endpoint_uri=uri)
|
||||
return provider
|
||||
|
||||
def connect_condition_providers(
|
||||
self, condition_provider_uris: Optional[Dict[int, List[str]]] = None
|
||||
) -> DefaultDict[int, Set[HTTPProvider]]:
|
||||
"""Multi-provider support"""
|
||||
|
||||
# If condition_provider_uris is None the node operator
|
||||
# did not configure any additional condition providers.
|
||||
condition_provider_uris = condition_provider_uris or dict()
|
||||
|
||||
# These are the chains that the Ritualist will connect to for conditions evaluation (read-only).
|
||||
condition_providers = defaultdict(set)
|
||||
|
||||
# Now, add any additional providers that were passed in.
|
||||
for chain_id, condition_provider_uris in condition_provider_uris.items():
|
||||
if not self._is_permitted_condition_chain(chain_id):
|
||||
# this is a safety check to prevent the Ritualist from connecting to
|
||||
# chains that are not supported by ursulas on the network;
|
||||
# Prevents the Ursula/Ritualist from starting up if this happens.
|
||||
raise NotImplementedError(
|
||||
f"Chain ID {chain_id} is not supported for condition evaluation by the Ritualist."
|
||||
)
|
||||
|
||||
providers = set()
|
||||
for uri in condition_provider_uris:
|
||||
provider = self._make_condition_provider(uri)
|
||||
providers.add(provider)
|
||||
|
||||
condition_providers[int(chain_id)] = providers
|
||||
|
||||
# Log the chains that the Ritualist is connected to.
|
||||
humanized_chain_ids = ", ".join(
|
||||
_CONDITION_CHAINS[chain_id] for chain_id in condition_providers
|
||||
)
|
||||
self.log.info(
|
||||
f"Connected to {len(condition_providers)} blockchains for condition checking: {humanized_chain_ids}"
|
||||
)
|
||||
|
||||
return condition_providers
|
||||
|
||||
def get_ritual(self, ritual_id: int) -> CoordinatorAgent.Ritual:
|
||||
try:
|
||||
ritual = self.ritual_tracker.rituals[ritual_id]
|
||||
except KeyError:
|
||||
raise self.ActorError(f"{ritual_id} is not in the local cache")
|
||||
return ritual
|
||||
|
||||
def _resolve_validators(
|
||||
self,
|
||||
ritual: CoordinatorAgent.Ritual,
|
||||
|
|
|
@ -68,6 +68,7 @@ POA_CHAINS = {
|
|||
42, # Kovan
|
||||
77, # Sokol
|
||||
100, # xDAI
|
||||
10200, # gnosis/chiado,
|
||||
137, # Polygon/Mainnet
|
||||
80001, # "Polygon/Mumbai"
|
||||
}
|
||||
|
|
|
@ -34,11 +34,18 @@ class NetworksInventory: # TODO: See #1564
|
|||
pass
|
||||
|
||||
@classmethod
|
||||
def get_ethereum_chain_id(cls, network): # TODO: Use this (where?) to make sure we're in the right chain
|
||||
def get_ethereum_chain_id(cls, network):
|
||||
try:
|
||||
return cls.__to_ethereum_chain_id[network]
|
||||
return cls.__to_chain_id_eth[network]
|
||||
except KeyError:
|
||||
return 1337 # TODO: what about chain id when testing?
|
||||
raise cls.UnrecognizedNetwork(network)
|
||||
|
||||
@classmethod
|
||||
def get_polygon_chain_id(cls, network):
|
||||
try:
|
||||
return cls.__to_chain_id_polygon[network]
|
||||
except KeyError:
|
||||
raise cls.UnrecognizedNetwork(network)
|
||||
|
||||
@classmethod
|
||||
def validate_network_name(cls, network_name: str):
|
||||
|
|
|
@ -820,6 +820,7 @@ class Ursula(Teacher, Character, actors.Operator, actors.Ritualist):
|
|||
client_password: Optional[str] = None,
|
||||
operator_signature_from_metadata=NOT_SIGNED,
|
||||
eth_provider_uri: Optional[str] = None,
|
||||
condition_provider_uris: Optional[Dict[int, List[str]]] = None,
|
||||
payment_method: Optional[Union[PaymentMethod, ContractPayment]] = None,
|
||||
# Character
|
||||
abort_on_learning_error: bool = False,
|
||||
|
@ -869,7 +870,8 @@ class Ursula(Teacher, Character, actors.Operator, actors.Ritualist):
|
|||
actors.Ritualist.__init__(
|
||||
self,
|
||||
domain=domain,
|
||||
provider_uri=payment_method.provider,
|
||||
condition_provider_uris=condition_provider_uris,
|
||||
coordinator_provider_uri=payment_method.provider,
|
||||
network=payment_method.network,
|
||||
transacting_power=self.transacting_power,
|
||||
crypto_power=self._crypto_power,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from copy import copy
|
||||
from unittest import mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from eth_tester.exceptions import ValidationError
|
||||
|
@ -10,7 +11,7 @@ from nucypher.config.constants import TEMPORARY_DOMAIN
|
|||
from nucypher.crypto.powers import CryptoPower
|
||||
from nucypher.exceptions import DevelopmentInstallationRequired
|
||||
from nucypher.policy.payment import FreeReencryptions
|
||||
from tests.constants import TEST_ETH_PROVIDER_URI
|
||||
from tests.constants import TEST_ETH_PROVIDER_URI, TESTERCHAIN_CHAIN_ID
|
||||
|
||||
|
||||
class Vladimir(Ursula):
|
||||
|
@ -52,6 +53,11 @@ class Vladimir(Ursula):
|
|||
bogus_payment_method.provider = Mock()
|
||||
bogus_payment_method.agent = Mock()
|
||||
bogus_payment_method.network = TEMPORARY_DOMAIN
|
||||
bogus_payment_method.agent.blockchain.client.chain_id = TESTERCHAIN_CHAIN_ID
|
||||
mock.patch(
|
||||
"mock.interfaces.MockBlockchain.client.chain_id",
|
||||
new_callable=mock.PropertyMock(return_value=TESTERCHAIN_CHAIN_ID),
|
||||
)
|
||||
|
||||
vladimir = cls(is_me=True,
|
||||
crypto_power=crypto_power,
|
||||
|
|
|
@ -304,7 +304,7 @@ class CharacterConfiguration(BaseConfiguration):
|
|||
'Sideways Engagement' of Character classes; a reflection of input parameters.
|
||||
"""
|
||||
|
||||
VERSION = 5 # bump when static payload scheme changes
|
||||
VERSION = 6 # bump when static payload scheme changes
|
||||
|
||||
CHARACTER_CLASS = NotImplemented
|
||||
MNEMONIC_KEYSTORE = False
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
|
||||
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from cryptography.x509 import Certificate
|
||||
from eth_utils import is_checksum_address
|
||||
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
from nucypher.config.base import CharacterConfiguration
|
||||
from nucypher.config.constants import (
|
||||
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD,
|
||||
|
@ -40,6 +38,7 @@ class UrsulaConfiguration(CharacterConfiguration):
|
|||
rest_port: Optional[int] = None,
|
||||
certificate: Optional[Certificate] = None,
|
||||
availability_check: Optional[bool] = None,
|
||||
condition_provider_uris: Optional[Dict[int, List[str]]] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
|
@ -58,8 +57,27 @@ class UrsulaConfiguration(CharacterConfiguration):
|
|||
self.rest_host = rest_host
|
||||
self.certificate = certificate
|
||||
self.operator_address = operator_address
|
||||
self.availability_check = availability_check if availability_check is not None else self.DEFAULT_AVAILABILITY_CHECKS
|
||||
super().__init__(dev_mode=dev_mode, keystore_path=keystore_path, *args, **kwargs)
|
||||
self.availability_check = (
|
||||
availability_check
|
||||
if availability_check is not None
|
||||
else self.DEFAULT_AVAILABILITY_CHECKS
|
||||
)
|
||||
super().__init__(
|
||||
dev_mode=dev_mode, keystore_path=keystore_path, *args, **kwargs
|
||||
)
|
||||
self.condition_provider_uris = condition_provider_uris or dict()
|
||||
self.configure_condition_provider_uris()
|
||||
|
||||
def configure_condition_provider_uris(self) -> None:
|
||||
"""Configure default condition provider URIs for mainnet and polygon network."""
|
||||
|
||||
# Polygon
|
||||
polygon_chain_id = NetworksInventory.get_polygon_chain_id(self.payment_network)
|
||||
self.condition_provider_uris[polygon_chain_id] = [self.payment_provider]
|
||||
|
||||
# Ethereum
|
||||
staking_chain_id = NetworksInventory.get_ethereum_chain_id(self.domain)
|
||||
self.condition_provider_uris[staking_chain_id] = [self.eth_provider_uri]
|
||||
|
||||
@classmethod
|
||||
def address_from_filepath(cls, filepath: Path) -> str:
|
||||
|
@ -85,12 +103,13 @@ class UrsulaConfiguration(CharacterConfiguration):
|
|||
rest_host=self.rest_host,
|
||||
rest_port=self.rest_port,
|
||||
availability_check=self.availability_check,
|
||||
condition_provider_uris=self.condition_provider_uris,
|
||||
|
||||
# PRE Payments
|
||||
# TODO: Resolve variable prefixing below (uses nested configuration fields?)
|
||||
payment_method=self.payment_method,
|
||||
payment_provider=self.payment_provider,
|
||||
payment_network=self.payment_network
|
||||
payment_network=self.payment_network,
|
||||
)
|
||||
return {**super().static_payload(), **payload}
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple
|
||||
|
||||
from eth_typing import ChecksumAddress
|
||||
from eth_utils import to_checksum_address
|
||||
from marshmallow import fields, post_load, validates_schema
|
||||
from web3 import Web3
|
||||
from web3 import HTTPProvider, Web3
|
||||
from web3.contract.contract import ContractFunction
|
||||
from web3.middleware import geth_poa_middleware
|
||||
from web3.providers import BaseProvider
|
||||
from web3.types import ABIFunction
|
||||
|
||||
from nucypher.blockchain.eth.clients import POA_CHAINS
|
||||
from nucypher.policy.conditions import STANDARD_ABI_CONTRACT_TYPES, STANDARD_ABIS
|
||||
from nucypher.policy.conditions.base import AccessControlCondition
|
||||
from nucypher.policy.conditions.context import get_context_value, is_context_variable
|
||||
|
@ -19,20 +21,27 @@ from nucypher.policy.conditions.exceptions import (
|
|||
from nucypher.policy.conditions.lingo import ReturnValueTest
|
||||
from nucypher.policy.conditions.utils import CamelCaseSchema, camel_case_to_snake
|
||||
|
||||
# TODO: Move this to a more appropriate location,
|
||||
# but be sure to change the mocks in tests too.
|
||||
# Permitted blockchains for condition evaluation
|
||||
_CONDITION_CHAINS = (
|
||||
1, # ethereum/mainnet
|
||||
5, # ethereum/goerli
|
||||
137, # polygon/mainnet
|
||||
80001 # polygon/mumbai
|
||||
)
|
||||
from nucypher.utilities import logging
|
||||
|
||||
_CONDITION_CHAINS = {
|
||||
1: "ethereum/mainnet",
|
||||
5: "ethereum/goerli",
|
||||
137: "polygon/mainnet",
|
||||
80001: "polygon/mumbai",
|
||||
# TODO: Permit support for these chains
|
||||
# 100: "gnosis/mainnet",
|
||||
# 10200: "gnosis/chiado",
|
||||
}
|
||||
|
||||
|
||||
def _resolve_abi(
|
||||
w3: Web3,
|
||||
method: str,
|
||||
standard_contract_type: Optional[str] = None,
|
||||
function_abi: Optional[ABIFunction] = None,
|
||||
w3: Web3,
|
||||
method: str,
|
||||
standard_contract_type: Optional[str] = None,
|
||||
function_abi: Optional[ABIFunction] = None,
|
||||
) -> ABIFunction:
|
||||
"""Resolves the contract an/or function ABI from a standard contract name"""
|
||||
|
||||
|
@ -53,7 +62,9 @@ def _resolve_abi(
|
|||
try:
|
||||
# Extract all function ABIs from the contract's ABI.
|
||||
# Will raise a ValueError if there is not exactly one match.
|
||||
function_abi = w3.eth.contract(abi=contract_abi).get_function_by_name(method).abi
|
||||
function_abi = (
|
||||
w3.eth.contract(abi=contract_abi).get_function_by_name(method).abi
|
||||
)
|
||||
except ValueError as e:
|
||||
raise InvalidCondition(str(e))
|
||||
|
||||
|
@ -61,7 +72,7 @@ def _resolve_abi(
|
|||
|
||||
|
||||
def _resolve_any_context_variables(
|
||||
parameters: List[Any], return_value_test: ReturnValueTest, **context
|
||||
parameters: List[Any], return_value_test: ReturnValueTest, **context
|
||||
):
|
||||
processed_parameters = []
|
||||
for p in parameters:
|
||||
|
@ -100,23 +111,27 @@ class RPCCondition(AccessControlCondition):
|
|||
|
||||
ALLOWED_METHODS = (
|
||||
# RPC
|
||||
'eth_getBalance',
|
||||
"eth_getBalance",
|
||||
) # TODO other allowed methods (tDEC #64)
|
||||
|
||||
LOG = logging.Logger(__name__)
|
||||
|
||||
class Schema(CamelCaseSchema):
|
||||
SKIP_VALUES = (None,)
|
||||
name = fields.Str(required=False)
|
||||
chain = fields.Int(required=True)
|
||||
method = fields.Str(required=True)
|
||||
parameters = fields.List(fields.Field, attribute='parameters', required=False)
|
||||
return_value_test = fields.Nested(ReturnValueTest.ReturnValueTestSchema(), required=True)
|
||||
parameters = fields.List(fields.Field, attribute="parameters", required=False)
|
||||
return_value_test = fields.Nested(
|
||||
ReturnValueTest.ReturnValueTestSchema(), required=True
|
||||
)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return RPCCondition(**data)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
r = f'{self.__class__.__name__}(function={self.method}, chain={self.chain})'
|
||||
r = f"{self.__class__.__name__}(function={self.method}, chain={self.chain})"
|
||||
return r
|
||||
|
||||
def __init__(
|
||||
|
@ -127,7 +142,6 @@ class RPCCondition(AccessControlCondition):
|
|||
name: Optional[str] = None,
|
||||
parameters: Optional[List[Any]] = None,
|
||||
):
|
||||
|
||||
# Validate input
|
||||
# TODO: Additional validation (function is valid for ABI, RVT validity, standard contract name validity, etc.)
|
||||
_validate_chain(chain=chain)
|
||||
|
@ -135,6 +149,7 @@ class RPCCondition(AccessControlCondition):
|
|||
# internal
|
||||
self.name = name
|
||||
self.chain = chain
|
||||
self.provider: Optional[BaseProvider] = None # set in _configure_provider
|
||||
self.method = self.validate_method(method=method)
|
||||
|
||||
# test
|
||||
|
@ -153,24 +168,49 @@ class RPCCondition(AccessControlCondition):
|
|||
)
|
||||
return method
|
||||
|
||||
def _configure_provider(self, providers: Dict[int, BaseProvider]):
|
||||
"""Binds the condition's contract function to a blockchian provider for evaluation"""
|
||||
def _next_endpoint(
|
||||
self, providers: Dict[int, Set[HTTPProvider]]
|
||||
) -> Iterator[HTTPProvider]:
|
||||
"""Yields the next web3 provider to try for a given chain ID"""
|
||||
try:
|
||||
provider = providers[self.chain]
|
||||
rpc_providers = providers[self.chain]
|
||||
|
||||
# if there are no entries for the chain ID, there
|
||||
# is no connection to that chain available.
|
||||
except KeyError:
|
||||
raise NoConnectionToChain(chain=self.chain)
|
||||
if not rpc_providers:
|
||||
raise NoConnectionToChain(chain=self.chain)
|
||||
for provider in rpc_providers:
|
||||
# Someday, we might make this whole function async, and then we can knock on
|
||||
# each endpoint here to see if it's alive and only yield it if it is.
|
||||
yield provider
|
||||
|
||||
def _configure_w3(self, provider: BaseProvider) -> Web3:
|
||||
# Instantiate a local web3 instance
|
||||
self.w3 = Web3(provider)
|
||||
self.provider = provider
|
||||
w3 = Web3(provider)
|
||||
if self.chain in POA_CHAINS:
|
||||
# inject web3 middleware to handle POA chain extra_data field.
|
||||
self.w3.middleware_onion.inject(geth_poa_middleware, layer=0)
|
||||
return w3
|
||||
|
||||
# This next block validates that the actual web3 provider is *actually*
|
||||
# connected to the condition's chain ID by reading its RPC endpoint.
|
||||
def _check_chain_id(self) -> None:
|
||||
"""
|
||||
Validates that the actual web3 provider is *actually*
|
||||
connected to the condition's chain ID by reading its RPC endpoint.
|
||||
"""
|
||||
provider_chain = self.w3.eth.chain_id
|
||||
if provider_chain != self.chain:
|
||||
raise InvalidCondition(
|
||||
f"This condition can only be evaluated on chain ID {self.chain} but the provider's "
|
||||
f"connection is to chain ID {provider_chain}"
|
||||
)
|
||||
|
||||
def _configure_provider(self, provider: BaseProvider):
|
||||
"""Binds the condition's contract function to a blockchain provider for evaluation"""
|
||||
self.w3 = self._configure_w3(provider=provider)
|
||||
self._check_chain_id()
|
||||
return provider
|
||||
|
||||
def _get_web3_py_function(self, rpc_method: str):
|
||||
|
@ -187,19 +227,31 @@ class RPCCondition(AccessControlCondition):
|
|||
rpc_result = rpc_function(*parameters) # RPC read
|
||||
return rpc_result
|
||||
|
||||
def verify(self, providers: Dict[int, BaseProvider], **context) -> Tuple[bool, Any]:
|
||||
def verify(
|
||||
self, providers: Dict[int, Set[HTTPProvider]], **context
|
||||
) -> Tuple[bool, Any]:
|
||||
"""
|
||||
Verifies the onchain condition is met by performing a
|
||||
read operation and evaluating the return value test.
|
||||
"""
|
||||
self._configure_provider(providers=providers)
|
||||
parameters, return_value_test = _resolve_any_context_variables(
|
||||
self.parameters, self.return_value_test, **context
|
||||
)
|
||||
try:
|
||||
result = self._execute_call(parameters=parameters)
|
||||
except Exception as e:
|
||||
raise RPCExecutionFailed(f"Contract call '{self.method}' failed: {e}")
|
||||
endpoints = self._next_endpoint(providers=providers)
|
||||
for provider in endpoints:
|
||||
self._configure_provider(provider=provider)
|
||||
parameters, return_value_test = _resolve_any_context_variables(
|
||||
self.parameters, self.return_value_test, **context
|
||||
)
|
||||
try:
|
||||
result = self._execute_call(parameters=parameters)
|
||||
break
|
||||
except Exception as e:
|
||||
self.LOG.warn(
|
||||
f"RPC call '{self.method}' failed: {e}, attempting to try next endpoint."
|
||||
)
|
||||
# Something went wrong. Try the next endpoint.
|
||||
continue
|
||||
else:
|
||||
# Fuck.
|
||||
raise RPCExecutionFailed(f"Contract call '{self.method}' failed.")
|
||||
|
||||
eval_result = return_value_test.eval(result) # test
|
||||
return eval_result, result
|
||||
|
@ -230,7 +282,7 @@ class ContractCondition(RPCCondition):
|
|||
standard_contract_type: Optional[str] = None,
|
||||
function_abi: Optional[ABIFunction] = None,
|
||||
*args,
|
||||
**kwargs
|
||||
**kwargs,
|
||||
):
|
||||
# internal
|
||||
super().__init__(*args, **kwargs)
|
||||
|
@ -251,9 +303,11 @@ class ContractCondition(RPCCondition):
|
|||
self.contract_function = self._get_unbound_contract_function()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
r = f'{self.__class__.__name__}(function={self.method}, ' \
|
||||
f'contract={self.contract_address[:6]}..., ' \
|
||||
f'chain={self.chain})'
|
||||
r = (
|
||||
f"{self.__class__.__name__}(function={self.method}, "
|
||||
f"contract={self.contract_address[:6]}..., "
|
||||
f"chain={self.chain})"
|
||||
)
|
||||
return r
|
||||
|
||||
def validate_method(self, method):
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
"""
|
||||
|
||||
import json
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
"""
|
||||
|
||||
import json
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import sys
|
||||
|
||||
BACKUP_SUFFIX = '.old'
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
|
||||
BACKUP_SUFFIX = ".old"
|
||||
OLD_VERSION = 4
|
||||
NEW_VERSION = 6
|
||||
|
||||
|
||||
def configuration_v4_to_v6(filepath: str):
|
||||
# Read + deserialize
|
||||
with open(filepath, "r") as file:
|
||||
contents = file.read()
|
||||
config = json.loads(contents)
|
||||
|
||||
try:
|
||||
existing_version = config["version"]
|
||||
if existing_version != OLD_VERSION:
|
||||
raise RuntimeError(
|
||||
f"Existing configuration is not version {OLD_VERSION}; Got version {existing_version}"
|
||||
)
|
||||
|
||||
# Make a copy of the original file
|
||||
backup_filepath = filepath + BACKUP_SUFFIX
|
||||
os.rename(filepath, backup_filepath)
|
||||
print(f"Backed up existing configuration to {backup_filepath}")
|
||||
|
||||
# Apply updates
|
||||
del config["federated_only"] # deprecated
|
||||
del config["checksum_address"]
|
||||
config["version"] = NEW_VERSION
|
||||
|
||||
# Multichain support
|
||||
eth_provider = config["eth_provider_uri"]
|
||||
eth_chain_id = NetworksInventory.get_ethereum_chain_id(config["domain"])
|
||||
polygon_provider = config["payment_provider"]
|
||||
polygon_chain_id = NetworksInventory.get_polygon_chain_id(
|
||||
config["payment_network"]
|
||||
)
|
||||
config["condition_providers"] = {
|
||||
eth_chain_id: [eth_provider],
|
||||
polygon_chain_id: [polygon_provider],
|
||||
}
|
||||
|
||||
except KeyError:
|
||||
raise RuntimeError(f"Invalid {OLD_VERSION} configuration file.")
|
||||
|
||||
# Commit updates
|
||||
with open(filepath, "w") as file:
|
||||
file.write(json.dumps(config, indent=4))
|
||||
print(f"OK! Migrated configuration file from v{OLD_VERSION} -> v{NEW_VERSION}.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
_python, filepath = sys.argv
|
||||
except ValueError:
|
||||
raise ValueError("Invalid command: Provide a single configuration filepath.")
|
||||
configuration_v4_to_v6(filepath=filepath)
|
|
@ -1,6 +1,5 @@
|
|||
import pytest
|
||||
|
||||
import nucypher
|
||||
from nucypher.blockchain.eth.agents import (
|
||||
ContractAgency,
|
||||
NucypherTokenAgent,
|
||||
|
@ -19,19 +18,10 @@ from tests.constants import TEST_ETH_PROVIDER_URI, TESTERCHAIN_CHAIN_ID
|
|||
|
||||
@pytest.fixture()
|
||||
def condition_providers(testerchain):
|
||||
providers = {testerchain.client.chain_id: testerchain.provider}
|
||||
providers = {testerchain.client.chain_id: {testerchain.provider}}
|
||||
return providers
|
||||
|
||||
|
||||
def mock_condition_blockchains(mocker):
|
||||
"""adds testerchain's chain ID to permitted conditional chains"""
|
||||
mocker.patch.object(
|
||||
nucypher.policy.conditions.evm,
|
||||
"_CONDITION_CHAINS",
|
||||
tuple([TESTERCHAIN_CHAIN_ID]),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def compound_lingo(
|
||||
erc721_evm_condition_balanceof,
|
||||
|
|
|
@ -165,7 +165,7 @@ def test_rpc_condition_evaluation_with_context_var_in_return_value_test(
|
|||
invalid_balance = balance + 1
|
||||
context[":balanceContextVar"] = invalid_balance
|
||||
condition_result, call_result = rpc_condition.verify(
|
||||
providers={testerchain.client.chain_id: testerchain.provider}, **context
|
||||
providers={testerchain.client.chain_id: [testerchain.provider]}, **context
|
||||
)
|
||||
assert condition_result is False
|
||||
assert call_result != invalid_balance
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
from collections import defaultdict
|
||||
|
||||
import pytest
|
||||
from web3 import HTTPProvider
|
||||
|
||||
from nucypher.policy.conditions.evm import _CONDITION_CHAINS, RPCCondition
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo
|
||||
from nucypher.utilities.logging import GlobalLoggerSettings
|
||||
from tests.constants import TESTERCHAIN_CHAIN_ID
|
||||
from tests.utils.policy import make_message_kits
|
||||
|
||||
GlobalLoggerSettings.start_text_file_logging()
|
||||
|
||||
|
||||
def make_multichain_evm_conditions(bob, chain_ids):
|
||||
"""This is a helper function to make a set of conditions that are valid on multiple chains."""
|
||||
operands = list()
|
||||
for chain_id in chain_ids:
|
||||
operand = [
|
||||
{
|
||||
"returnValueTest": {"value": "0", "comparator": ">"},
|
||||
"method": "blocktime",
|
||||
"chain": chain_id,
|
||||
},
|
||||
{
|
||||
"chain": chain_id,
|
||||
"method": "eth_getBalance",
|
||||
"parameters": [bob.checksum_address, "latest"],
|
||||
"returnValueTest": {"comparator": ">=", "value": "10000000000000"},
|
||||
},
|
||||
]
|
||||
operands.extend(operand)
|
||||
|
||||
_conditions = {
|
||||
"version": ConditionLingo.VERSION,
|
||||
"condition": {
|
||||
"operator": "and",
|
||||
"operands": operands,
|
||||
},
|
||||
}
|
||||
return _conditions
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def chain_ids(module_mocker):
|
||||
ids = [
|
||||
TESTERCHAIN_CHAIN_ID,
|
||||
TESTERCHAIN_CHAIN_ID + 1,
|
||||
TESTERCHAIN_CHAIN_ID + 2,
|
||||
123456789,
|
||||
]
|
||||
module_mocker.patch.dict(
|
||||
_CONDITION_CHAINS, {cid: "fakechain/mainnet" for cid in ids}
|
||||
)
|
||||
return ids
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def multichain_ursulas(ursulas, chain_ids):
|
||||
base_uri = "tester://multichain.{}"
|
||||
base_fallback_uri = "tester://multichain.fallback.{}"
|
||||
provider_uris = [base_uri.format(i) for i in range(len(chain_ids))]
|
||||
fallback_provider_uris = [
|
||||
base_fallback_uri.format(i) for i in range(len(chain_ids))
|
||||
]
|
||||
mocked_condition_providers = {
|
||||
cid: {HTTPProvider(uri), HTTPProvider(furi)}
|
||||
for cid, uri, furi in zip(chain_ids, provider_uris, fallback_provider_uris)
|
||||
}
|
||||
for ursula in ursulas:
|
||||
ursula.condition_providers = mocked_condition_providers
|
||||
return ursulas
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def conditions(bob, chain_ids):
|
||||
_conditions = make_multichain_evm_conditions(bob, chain_ids)
|
||||
return _conditions
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def monkeymodule():
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
mpatch = MonkeyPatch()
|
||||
yield mpatch
|
||||
mpatch.undo()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def mock_rpc_condition(module_mocker, testerchain, monkeymodule):
|
||||
def configure_mock(condition, provider, *args, **kwargs):
|
||||
condition.provider = provider
|
||||
return testerchain.w3
|
||||
|
||||
monkeymodule.setattr(RPCCondition, "_configure_w3", configure_mock)
|
||||
configure_spy = module_mocker.spy(RPCCondition, "_configure_w3")
|
||||
|
||||
chain_id_check_mock = module_mocker.patch.object(RPCCondition, "_check_chain_id")
|
||||
return configure_spy, chain_id_check_mock
|
||||
|
||||
|
||||
def test_single_retrieve_with_multichain_conditions(
|
||||
enacted_policy, bob, multichain_ursulas, conditions, mock_rpc_condition, mocker
|
||||
):
|
||||
bob.remember_node(multichain_ursulas[0])
|
||||
bob.start_learning_loop()
|
||||
|
||||
messages, message_kits = make_message_kits(enacted_policy.public_key, conditions)
|
||||
policy_info_kwargs = dict(
|
||||
encrypted_treasure_map=enacted_policy.treasure_map,
|
||||
alice_verifying_key=enacted_policy.publisher_verifying_key,
|
||||
)
|
||||
|
||||
cleartexts = bob.retrieve_and_decrypt(
|
||||
message_kits=message_kits,
|
||||
**policy_info_kwargs,
|
||||
)
|
||||
|
||||
assert cleartexts == messages
|
||||
|
||||
|
||||
def test_single_decryption_request_with_faulty_rpc_endpoint(
|
||||
enacted_policy, bob, multichain_ursulas, conditions, mock_rpc_condition
|
||||
):
|
||||
bob.remember_node(multichain_ursulas[0])
|
||||
bob.start_learning_loop()
|
||||
|
||||
messages, message_kits = make_message_kits(enacted_policy.public_key, conditions)
|
||||
policy_info_kwargs = dict(
|
||||
encrypted_treasure_map=enacted_policy.treasure_map,
|
||||
alice_verifying_key=enacted_policy.publisher_verifying_key,
|
||||
)
|
||||
|
||||
calls = defaultdict(int)
|
||||
original_execute_call = RPCCondition._execute_call
|
||||
|
||||
def faulty_execute_call(*args, **kwargs):
|
||||
"""Intercept the call to the RPC endpoint and raise an exception on the second call."""
|
||||
nonlocal calls
|
||||
rpc_call = args[0]
|
||||
calls[rpc_call.chain] += 1
|
||||
if (
|
||||
calls[rpc_call.chain] == 2
|
||||
and "tester://multichain.0" in rpc_call.provider.endpoint_uri
|
||||
):
|
||||
# simulate a network error
|
||||
raise ConnectionError("Something went wrong with the network")
|
||||
elif calls[rpc_call.chain] == 3:
|
||||
# check the provider is the fallback
|
||||
this_uri = rpc_call.provider.endpoint_uri
|
||||
assert "fallback" in this_uri
|
||||
return original_execute_call(*args, **kwargs)
|
||||
|
||||
RPCCondition._execute_call = faulty_execute_call
|
||||
cleartexts = bob.retrieve_and_decrypt(
|
||||
message_kits=message_kits,
|
||||
**policy_info_kwargs,
|
||||
)
|
||||
assert cleartexts == messages
|
||||
RPCCondition._execute_call = original_execute_call
|
|
@ -4,9 +4,10 @@ import random
|
|||
import pytest
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.blockchain.eth.actors import Operator
|
||||
from nucypher.blockchain.eth.actors import Operator, Ritualist
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, PREApplicationAgent
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.config.constants import TEMPORARY_DOMAIN
|
||||
from nucypher.crypto.powers import CryptoPower, TransactingPower
|
||||
|
@ -18,6 +19,7 @@ from tests.constants import (
|
|||
MIN_STAKE_FOR_TESTS,
|
||||
MOCK_STAKING_CONTRACT_NAME,
|
||||
TEST_ETH_PROVIDER_URI,
|
||||
TESTERCHAIN_CHAIN_ID,
|
||||
)
|
||||
from tests.utils.ape import deploy_contracts as ape_deploy_contracts
|
||||
from tests.utils.ape import registry_from_ape_deployments
|
||||
|
@ -26,6 +28,30 @@ from tests.utils.blockchain import TesterBlockchain
|
|||
test_logger = Logger("acceptance-test-logger")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def mock_condition_blockchains(session_mocker):
|
||||
"""adds testerchain's chain ID to permitted conditional chains"""
|
||||
session_mocker.patch.dict(
|
||||
"nucypher.policy.conditions.evm._CONDITION_CHAINS",
|
||||
{TESTERCHAIN_CHAIN_ID: "eth-tester/pyevm"},
|
||||
)
|
||||
|
||||
session_mocker.patch.object(
|
||||
NetworksInventory, "get_polygon_chain_id", return_value=TESTERCHAIN_CHAIN_ID
|
||||
)
|
||||
|
||||
session_mocker.patch.object(
|
||||
NetworksInventory, "get_ethereum_chain_id", return_value=TESTERCHAIN_CHAIN_ID
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def mock_multichain_configuration(module_mocker, testerchain):
|
||||
module_mocker.patch.object(
|
||||
Ritualist, "_make_condition_provider", return_value=testerchain.provider
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope='session', autouse=True)
|
||||
def nucypher_contracts(project):
|
||||
nucypher_contracts_dependency_api = project.dependencies["nucypher-contracts"]
|
||||
|
|
|
@ -3,11 +3,13 @@ from collections import defaultdict
|
|||
import pytest
|
||||
from eth_utils.crypto import keccak
|
||||
|
||||
from nucypher.blockchain.eth.actors import Ritualist
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.network.nodes import Learner
|
||||
from nucypher.network.trackers import AvailabilityTracker
|
||||
from nucypher.utilities.logging import GlobalLoggerSettings
|
||||
from tests.constants import MOCK_IP_ADDRESS
|
||||
from tests.constants import MOCK_IP_ADDRESS, TESTERCHAIN_CHAIN_ID
|
||||
|
||||
# Don't re-lock accounts in the background while making commitments
|
||||
LOCK_FUNCTION = TransactingPower.lock_account
|
||||
|
@ -135,3 +137,27 @@ def mock_get_external_ip_from_url_source(session_mocker):
|
|||
def disable_check_grant_requirements(session_mocker):
|
||||
target = 'nucypher.characters.lawful.Alice._check_grant_requirements'
|
||||
session_mocker.patch(target, return_value=MOCK_IP_ADDRESS)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def mock_condition_blockchains(session_mocker):
|
||||
"""adds testerchain's chain ID to permitted conditional chains"""
|
||||
session_mocker.patch.dict(
|
||||
"nucypher.policy.conditions.evm._CONDITION_CHAINS",
|
||||
{TESTERCHAIN_CHAIN_ID: "eth-tester/pyevm"},
|
||||
)
|
||||
|
||||
session_mocker.patch.object(
|
||||
NetworksInventory, "get_polygon_chain_id", return_value=TESTERCHAIN_CHAIN_ID
|
||||
)
|
||||
|
||||
session_mocker.patch.object(
|
||||
NetworksInventory, "get_ethereum_chain_id", return_value=TESTERCHAIN_CHAIN_ID
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", autouse=True)
|
||||
def mock_multichain_configuration(module_mocker, testerchain):
|
||||
module_mocker.patch.object(
|
||||
Ritualist, "_make_condition_provider", return_value=testerchain.provider
|
||||
)
|
||||
|
|
|
@ -13,16 +13,10 @@ import pytest
|
|||
from click.testing import CliRunner
|
||||
from eth_account import Account
|
||||
from eth_utils import to_checksum_address
|
||||
from nucypher_core.ferveo import (
|
||||
AggregatedTranscript,
|
||||
DkgPublicKey,
|
||||
Keypair,
|
||||
Validator,
|
||||
)
|
||||
from nucypher_core.ferveo import AggregatedTranscript, DkgPublicKey, Keypair, Validator
|
||||
from twisted.internet.task import Clock
|
||||
from web3 import Web3
|
||||
|
||||
import nucypher
|
||||
import tests
|
||||
from nucypher.blockchain.economics import Economics
|
||||
from nucypher.blockchain.eth.clients import EthereumClient
|
||||
|
@ -42,7 +36,7 @@ from nucypher.crypto.ferveo import dkg
|
|||
from nucypher.crypto.keystore import Keystore
|
||||
from nucypher.network.nodes import TEACHER_NODES
|
||||
from nucypher.policy.conditions.context import USER_ADDRESS_CONTEXT
|
||||
from nucypher.policy.conditions.evm import ContractCondition, RPCCondition
|
||||
from nucypher.policy.conditions.evm import RPCCondition
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo, ReturnValueTest
|
||||
from nucypher.policy.conditions.time import TimeCondition
|
||||
from nucypher.policy.payment import SubscriptionManagerPayment
|
||||
|
@ -605,14 +599,6 @@ def conditions_test_data():
|
|||
return data
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_condition_blockchains(mocker):
|
||||
"""adds testerchain's chain ID to permitted conditional chains"""
|
||||
mocker.patch.object(
|
||||
nucypher.policy.conditions.evm, "_CONDITION_CHAINS", tuple([TESTERCHAIN_CHAIN_ID])
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def time_condition():
|
||||
condition = TimeCondition(
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import pytest
|
||||
|
||||
|
||||
def test_new_ursula_announces_herself(
|
||||
lonely_ursula_maker, test_registry_source_manager
|
||||
):
|
||||
|
@ -27,3 +30,18 @@ def test_node_deployer(ursulas):
|
|||
deployer = ursula.get_deployer()
|
||||
assert deployer.options['https_port'] == ursula.rest_information()[0].port
|
||||
assert deployer.application == ursula.rest_app
|
||||
|
||||
|
||||
def test_goerli_and_mumbai_as_conditions_providers(
|
||||
lonely_ursula_maker, test_registry_source_manager
|
||||
):
|
||||
INVALID_CHAIN_ID = 66775827584859395569954838 # If we eventually support a chain with this ID, heaven help us.
|
||||
|
||||
with pytest.raises(NotImplementedError):
|
||||
_ursula_who_tries_to_connect_to_an_invalid_chain = lonely_ursula_maker(
|
||||
quantity=1,
|
||||
domain="useless_domain",
|
||||
condition_provider_uris={
|
||||
INVALID_CHAIN_ID: "this is a provider URI, but it doesn't matter what we pass here because the chain_id is invalid."
|
||||
},
|
||||
)
|
||||
|
|
|
@ -5,6 +5,8 @@ from unittest.mock import PropertyMock
|
|||
|
||||
import pytest
|
||||
|
||||
import nucypher
|
||||
from nucypher.blockchain.eth.actors import Ritualist
|
||||
from nucypher.blockchain.eth.trackers.dkg import ActiveRitualTracker
|
||||
from nucypher.cli.literature import (
|
||||
COLLECT_NUCYPHER_PASSWORD,
|
||||
|
@ -31,6 +33,7 @@ from tests.constants import (
|
|||
MOCK_ETH_PROVIDER_URI,
|
||||
MOCK_IP_ADDRESS,
|
||||
YES_ENTER,
|
||||
TESTERCHAIN_CHAIN_ID,
|
||||
)
|
||||
from tests.utils.ursula import select_test_port
|
||||
|
||||
|
@ -180,7 +183,9 @@ def test_ursula_view_configuration(custom_filepath: Path, click_runner, nominal_
|
|||
assert custom_config_filepath.is_file(), 'Configuration file does not exist'
|
||||
|
||||
|
||||
def test_run_ursula_from_config_file(custom_filepath: Path, click_runner, mock_funding_and_bonding):
|
||||
def test_run_ursula_from_config_file(
|
||||
custom_filepath: Path, click_runner, mock_funding_and_bonding, mocker
|
||||
):
|
||||
|
||||
# Ensure the configuration file still exists
|
||||
custom_config_filepath = custom_filepath / UrsulaConfiguration.generate_filename()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
@ -28,15 +29,18 @@ characters = (Alice, Bob, Ursula)
|
|||
|
||||
# Assemble
|
||||
characters_and_configurations = list(zip(characters, configurations))
|
||||
all_characters = tuple(characters, )
|
||||
all_configurations = tuple(configurations, )
|
||||
all_characters = tuple(
|
||||
characters,
|
||||
)
|
||||
all_configurations = tuple(
|
||||
configurations,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("character,configuration", characters_and_configurations)
|
||||
def test_development_character_configurations(
|
||||
character, configuration, test_registry_source_manager, mocker, testerchain
|
||||
):
|
||||
|
||||
mocker.patch.object(
|
||||
CharacterConfiguration, "DEFAULT_PAYMENT_NETWORK", TEMPORARY_DOMAIN
|
||||
)
|
||||
|
@ -73,7 +77,7 @@ def test_development_character_configurations(
|
|||
|
||||
# Node Storage
|
||||
assert isinstance(thing_one.node_storage, ForgetfulNodeStorage)
|
||||
assert ':memory:' in thing_one.node_storage._name
|
||||
assert ":memory:" in thing_one.node_storage._name
|
||||
|
||||
# All development characters are unique
|
||||
_characters = [thing_one, thing_two]
|
||||
|
@ -95,15 +99,16 @@ def test_default_character_configuration_preservation(
|
|||
tmpdir,
|
||||
test_registry,
|
||||
):
|
||||
|
||||
configuration_class.DEFAULT_CONFIG_ROOT = Path('/tmp')
|
||||
fake_address = '0xdeadbeef'
|
||||
configuration_class.DEFAULT_CONFIG_ROOT = Path("/tmp")
|
||||
fake_address = "0xdeadbeef"
|
||||
network = TEMPORARY_DOMAIN
|
||||
|
||||
expected_filename = f'{configuration_class.NAME}.{configuration_class._CONFIG_FILE_EXTENSION}'
|
||||
expected_filename = (
|
||||
f"{configuration_class.NAME}.{configuration_class._CONFIG_FILE_EXTENSION}"
|
||||
)
|
||||
generated_filename = configuration_class.generate_filename()
|
||||
assert generated_filename == expected_filename
|
||||
expected_filepath = Path('/', 'tmp', generated_filename)
|
||||
expected_filepath = Path("/", "tmp", generated_filename)
|
||||
|
||||
if expected_filepath.exists():
|
||||
expected_filepath.unlink()
|
||||
|
@ -112,7 +117,9 @@ def test_default_character_configuration_preservation(
|
|||
if configuration_class == UrsulaConfiguration:
|
||||
# special case for rest_host & dev mode
|
||||
# use keystore
|
||||
keystore = Keystore.generate(password=INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir)
|
||||
keystore = Keystore.generate(
|
||||
password=INSECURE_DEVELOPMENT_PASSWORD, keystore_dir=tmpdir
|
||||
)
|
||||
keystore.signing_public_key = SecretKey.random().public_key()
|
||||
character_config = configuration_class(
|
||||
checksum_address=fake_address,
|
||||
|
@ -143,12 +150,16 @@ def test_default_character_configuration_preservation(
|
|||
|
||||
try:
|
||||
# Read
|
||||
with open(character_config.filepath, 'r') as f:
|
||||
contents = f.read()
|
||||
with open(character_config.filepath, "r") as f:
|
||||
_contents = json.loads(
|
||||
f.read()
|
||||
) # ensure this can be read and is valid JSON
|
||||
|
||||
# Restore from JSON file
|
||||
restored_configuration = configuration_class.from_configuration_file()
|
||||
assert character_config.serialize() == restored_configuration.serialize()
|
||||
assert json.loads(character_config.serialize()) == json.loads(
|
||||
restored_configuration.serialize()
|
||||
)
|
||||
|
||||
# File still exists after reading
|
||||
assert written_filepath.exists()
|
||||
|
@ -183,7 +194,7 @@ def test_ursula_development_configuration(test_registry_source_manager, testerch
|
|||
assert port == UrsulaConfiguration.DEFAULT_DEVELOPMENT_REST_PORT
|
||||
assert ursula_one.certificate_filepath is CERTIFICATE_NOT_SAVED
|
||||
assert isinstance(ursula_one.node_storage, ForgetfulNodeStorage)
|
||||
assert ':memory:' in ursula_one.node_storage._name
|
||||
assert ":memory:" in ursula_one.node_storage._name
|
||||
|
||||
# Alternate way to produce a character with a direct call
|
||||
ursula_two = config.produce()
|
||||
|
@ -201,19 +212,15 @@ def test_ursula_development_configuration(test_registry_source_manager, testerch
|
|||
|
||||
|
||||
@pytest.mark.skip("See #2016")
|
||||
def test_destroy_configuration(config,
|
||||
test_emitter,
|
||||
capsys,
|
||||
mocker):
|
||||
def test_destroy_configuration(config, test_emitter, capsys, mocker):
|
||||
# Setup
|
||||
config_class = config.__class__
|
||||
config_file = config.filepath
|
||||
|
||||
# Isolate from filesystem and Spy on the methods we're testing here
|
||||
spy_keystore_attached = mocker.spy(CharacterConfiguration, 'attach_keystore')
|
||||
mock_config_destroy = mocker.patch.object(CharacterConfiguration, 'destroy')
|
||||
spy_keystore_destroy = mocker.spy(Keystore, 'destroy')
|
||||
mock_os_remove = mocker.patch('pathlib.Path.unlink')
|
||||
spy_keystore_attached = mocker.spy(CharacterConfiguration, "attach_keystore")
|
||||
mock_config_destroy = mocker.patch.object(CharacterConfiguration, "destroy")
|
||||
spy_keystore_destroy = mocker.spy(Keystore, "destroy")
|
||||
mock_os_remove = mocker.patch("pathlib.Path.unlink")
|
||||
|
||||
# Test
|
||||
destroy_configuration(emitter=test_emitter, character_config=config)
|
||||
|
|
|
@ -4,7 +4,7 @@ from pathlib import Path
|
|||
from typing import Iterable, Optional
|
||||
|
||||
from nucypher.blockchain.economics import EconomicsFactory
|
||||
from nucypher.blockchain.eth.actors import Operator
|
||||
from nucypher.blockchain.eth.actors import Operator, Ritualist
|
||||
from nucypher.blockchain.eth.agents import (
|
||||
AdjudicatorAgent,
|
||||
ContractAgency,
|
||||
|
@ -15,6 +15,7 @@ from nucypher.blockchain.eth.interfaces import (
|
|||
BlockchainInterface,
|
||||
BlockchainInterfaceFactory,
|
||||
)
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
|
||||
from nucypher.blockchain.eth.signers import KeystoreSigner
|
||||
from nucypher.characters.lawful import Ursula
|
||||
|
@ -26,6 +27,7 @@ from tests.constants import (
|
|||
KEYFILE_NAME_TEMPLATE,
|
||||
MOCK_KEYSTORE_PATH,
|
||||
NUMBER_OF_MOCK_KEYSTORE_ACCOUNTS,
|
||||
TESTERCHAIN_CHAIN_ID,
|
||||
)
|
||||
from tests.mock.interfaces import MockBlockchain, mock_registry_source_manager
|
||||
from tests.mock.io import MockStdinWrapper
|
||||
|
@ -245,3 +247,20 @@ def staking_providers(testerchain, test_registry, monkeymodule):
|
|||
|
||||
Operator.get_staking_provider_address = faked
|
||||
return testerchain.stake_providers_accounts
|
||||
|
||||
|
||||
@pytest.fixture(scope="session", autouse=True)
|
||||
def mock_condition_blockchains(session_mocker):
|
||||
"""adds testerchain's chain ID to permitted conditional chains"""
|
||||
session_mocker.patch.dict(
|
||||
"nucypher.policy.conditions.evm._CONDITION_CHAINS",
|
||||
{TESTERCHAIN_CHAIN_ID: "eth-tester/pyevm"},
|
||||
)
|
||||
|
||||
session_mocker.patch.object(
|
||||
NetworksInventory, "get_polygon_chain_id", return_value=TESTERCHAIN_CHAIN_ID
|
||||
)
|
||||
|
||||
session_mocker.patch.object(
|
||||
NetworksInventory, "get_ethereum_chain_id", return_value=TESTERCHAIN_CHAIN_ID
|
||||
)
|
||||
|
|
|
@ -15,7 +15,7 @@ from nucypher.blockchain.eth.registry import (
|
|||
RegistrySourceManager,
|
||||
)
|
||||
from nucypher.config.constants import TEMPORARY_DOMAIN
|
||||
from tests.constants import MOCK_ETH_PROVIDER_URI
|
||||
from tests.constants import MOCK_ETH_PROVIDER_URI, TESTERCHAIN_CHAIN_ID
|
||||
from tests.utils.blockchain import TesterBlockchain
|
||||
|
||||
|
||||
|
@ -90,3 +90,6 @@ class MockEthereumClient(EthereumClient):
|
|||
def add_middleware(self, middleware):
|
||||
pass
|
||||
|
||||
@property
|
||||
def chain_id(self) -> int:
|
||||
return TESTERCHAIN_CHAIN_ID
|
||||
|
|
Loading…
Reference in New Issue