class ScenarioRunner: def __init__( self, account: Account, chain_urls: Dict[str, List[str]], auth: str, data_path: Path, scenario_file: Path, task_state_callback: Optional[ Callable[["ScenarioRunner", "Task", "TaskState"], None] ] = None, ): from scenario_player.node_support import RaidenReleaseKeeper, NodeController self.auth = auth self.release_keeper = RaidenReleaseKeeper(data_path.joinpath("raiden_releases")) self.task_count = 0 self.running_task_count = 0 self.task_cache = {} self.task_state_callback = task_state_callback # Storage for arbitrary data tasks might need to persist self.task_storage = defaultdict(dict) scenario_name = scenario_file.stem self.data_path = data_path.joinpath("scenarios", scenario_name) self.data_path.mkdir(exist_ok=True, parents=True) self.yaml = ScenarioYAML(scenario_file, self.data_path) log.debug("Data path", path=self.data_path) # Determining the run number requires :attr:`.data_path` self.run_number = self.determine_run_number() self.node_controller = NodeController(self, self.yaml.nodes) self.protocol = "http" self.gas_limit = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL * 2 self.chain_name, chain_urls = self.select_chain(chain_urls) self.eth_rpc_urls = chain_urls self.client = JSONRPCClient( Web3(HTTPProvider(chain_urls[0])), privkey=account.privkey, gas_price_strategy=self.yaml.settings.gas_price_strategy, ) self.chain_id = int(self.client.web3.net.version) self.contract_manager = ContractManager(contracts_precompiled_path()) balance = self.client.balance(account.address) if balance < OWN_ACCOUNT_BALANCE_MIN: raise ScenarioError( f"Insufficient balance ({balance / 10 ** 18} Eth) " f'in account {to_checksum_address(account.address)} on chain "{self.chain_name}"' ) self.session = Session() if auth: self.session.auth = tuple(auth.split(":")) self.session.mount("http", TimeOutHTTPAdapter(timeout=self.yaml.settings.timeout)) self.session.mount("https", TimeOutHTTPAdapter(timeout=self.yaml.settings.timeout)) self.service_session = ServiceInterface(self.yaml.spaas) # Request an RPC Client instance ID from the RPC service and assign it to the runner. assign_rpc_instance_id(self, chain_urls[0], account.privkey, self.yaml.settings.gas_price) self.token = Token(self, data_path) self.udc = None self.token_network_address = None task_config = self.yaml.scenario.root_config task_class = self.yaml.scenario.root_class self.root_task = task_class(runner=self, config=task_config) def determine_run_number(self) -> int: """Determine the current run number. We check for a run number file, and use any number that is logged there after incrementing it. REFAC: Replace this with a property. """ run_number = 0 run_number_file = self.data_path.joinpath("run_number.txt") if run_number_file.exists(): run_number = int(run_number_file.read_text()) + 1 run_number_file.write_text(str(run_number)) log.info("Run number", run_number=run_number) return run_number def select_chain(self, chain_urls: Dict[str, List[str]]) -> Tuple[str, List[str]]: """Select a chain and return its name and RPC URL. If the currently loaded scenario's designated chain is set to 'any', we randomly select a chain from the given `chain_urls`. Otherwise, we will return `ScenarioRunner.scenario.chain_name` and whatever value may be associated with this key in `chain_urls`. :raises ScenarioError: if ScenarioRunner.scenario.chain_name is not one of `('any', 'Any', 'ANY')` and it is not a key in `chain_urls`. """ chain_name = self.yaml.settings.chain if chain_name in ("any", "Any", "ANY"): chain_name = random.choice(list(chain_urls.keys())) log.info("Using chain", chain=chain_name) try: return chain_name, chain_urls[chain_name] except KeyError: raise ScenarioError( f'The scenario requested chain "{chain_name}" for which no RPC-URL is known.' ) def wait_for_token_network_discovery(self, node): """Check for token network discovery with the given `node`. By default exit the wait if the token has not been discovered after `n` seconds, where `n` is the value of :attr:`.timeout`. :raises TokenNetworkDiscoveryTimeout: If we waited a set time for the token network to be discovered, but it wasn't. """ log.info("Waiting till new network is found by nodes") node_endpoint = API_URL_TOKEN_NETWORK_ADDRESS.format( protocol=self.protocol, target_host=node, token_address=self.token.address ) started = time.monotonic() elapsed = 0 while elapsed < self.yaml.settings.timeout: try: resp = self.session.get(node_endpoint) resp.raise_for_status() except HTTPError as e: # We explicitly handle 404 Not Found responses only - anything else is none # of our business. if e.response.status_code != 404: raise # Wait before continuing, no sense in spamming the node. gevent.sleep(1) # Update our elapsed time tracker. elapsed = time.monotonic() - started continue else: # The node appears to have discovered our token network. data = resp.json() if not is_checksum_address(data): # Something's amiss about this response. Notify a human. raise TypeError(f"Unexpected response type from API: {data!r}") return data # We could not assert that our token network was registered within an # acceptable time frame. raise TokenNetworkDiscoveryTimeout def run_scenario(self): mint_gas = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL * 2 fund_tx, node_starter, node_addresses, node_count = self._initialize_nodes() ud_token_tx, udc_ctr, should_deposit_ud_token = self._initialize_udc( gas_limit=mint_gas, node_count=node_count ) mint_tx = self._initialize_scenario_token( node_addresses=node_addresses, udc_ctr=udc_ctr, should_deposit_ud_token=should_deposit_ud_token, gas_limit=mint_gas, ) wait_for_txs(self.client, fund_tx | ud_token_tx | mint_tx) if node_starter is not None: log.debug("Waiting for nodes to finish starting") node_starter.get(block=True) first_node = self.get_node_baseurl(0) registered_tokens = set( self.session.get( API_URL_TOKENS.format(protocol=self.protocol, target_host=first_node) ).json() ) if self.token.checksum_address not in registered_tokens: for _ in range(5): code, msg = self.register_token(self.token.checksum_address, first_node) if 199 < code < 300: break gevent.sleep(1) else: log.error("Couldn't register token with network", code=code, message=msg) raise TokenRegistrationError(msg) last_node = self.node_controller[-1].base_url self.token_network_address = self.wait_for_token_network_discovery(last_node) log.info( "Received token network address", token_network_address=self.token_network_address ) # Start root task root_task_greenlet = gevent.spawn(self.root_task) greenlets = {root_task_greenlet} greenlets.add(self.node_controller.start_node_monitor()) try: gevent.joinall(greenlets, raise_error=True) except BaseException: if not root_task_greenlet.dead: # Make sure we kill the tasks if a node dies root_task_greenlet.kill() raise def _initialize_scenario_token( self, node_addresses: Set[ChecksumAddress], udc_ctr: Optional[ContractProxy], should_deposit_ud_token: bool, gas_limit: int, ) -> Set[TransactionHash]: """Deploy new token to the blockchain, or load an existing one's data from disk.""" self.token.init() mint_tx = set() for address in node_addresses: tx = self.token.mint(address) if tx: mint_tx.add(tx) if not should_deposit_ud_token: continue deposit_tx = self.udc.deposit(address) if deposit_tx: mint_tx.add(deposit_tx) return mint_tx def _initialize_udc( self, gas_limit: int, node_count: int ) -> Tuple[Set[TransactionHash], Optional[ContractProxy], bool]: our_address = to_checksum_address(self.client.address) udc_settings = self.yaml.settings.services.udc udc_enabled = udc_settings.enable ud_token_tx = set() if not udc_enabled: return ud_token_tx, None, False udc_ctr, ud_token_ctr = get_udc_and_token(self) ud_token_address = to_checksum_address(ud_token_ctr.contract_address) udc_address = to_checksum_address(udc_ctr.contract_address) log.info("UDC enabled", contract_address=udc_address, token_address=ud_token_address) self.udc = UserDepositContract(self, udc_ctr, ud_token_ctr) should_deposit_ud_token = udc_enabled and udc_settings.token["deposit"] allowance_tx = self.udc.update_allowance() if allowance_tx: ud_token_tx.add(allowance_tx) if should_deposit_ud_token: tx = self.udc.mint(our_address) if tx: ud_token_tx.add(tx) return ud_token_tx, udc_ctr, should_deposit_ud_token def _initialize_nodes( self ) -> Tuple[Set[TransactionHash], gevent.Greenlet, Set[ChecksumAddress], int]: fund_tx = set() self.node_controller.initialize_nodes() node_addresses = self.node_controller.addresses node_count = len(self.node_controller) balance_per_node = {address: self.client.balance(address) for address in node_addresses} low_balances = { address: balance for address, balance in balance_per_node.items() if balance < NODE_ACCOUNT_BALANCE_MIN } if low_balances: log.info("Funding nodes", nodes=low_balances.keys()) fund_tx = set() for address, balance in low_balances.items(): params = { "client_id": self.yaml.spaas.rpc.client_id, "to": address, "value": NODE_ACCOUNT_BALANCE_FUND - balance, "startgas": 21_000, } resp = self.service_session.post("spaas://rpc/transactions", json=params) tx_hash = resp.json()["tx_hash"] fund_tx.add(tx_hash) node_starter = self.node_controller.start(wait=False) return fund_tx, node_starter, node_addresses, node_count def task_state_changed(self, task: "Task", state: "TaskState"): if self.task_state_callback: self.task_state_callback(self, task, state) def register_token(self, token_address, node): # TODO: Move this to :class:`scenario_player.utils.token.Token`. try: base_url = API_URL_TOKENS.format(protocol=self.protocol, target_host=node) url = "{}/{}".format(base_url, token_address) log.info("Registering token with network", url=url) resp = self.session.put(url) code = resp.status_code msg = resp.text except RequestException as ex: code = -1 msg = str(ex) return code, msg @staticmethod def _spawn_and_wait(objects, callback): tasks = {obj: gevent.spawn(callback, obj) for obj in objects} gevent.joinall(set(tasks.values())) return {obj: task.get() for obj, task in tasks.items()} def get_node_address(self, index): return self.node_controller[index].address def get_node_baseurl(self, index): return self.node_controller[index].base_url
class ScenarioRunner(object): def __init__( self, account: Account, chain_urls: Dict[str, List[str]], auth: str, data_path: Path, scenario_file: Path, ): from scenario_player.tasks.base import get_task_class_for_type from scenario_player.node_support import RaidenReleaseKeeper, NodeController self.task_count = 0 self.running_task_count = 0 self.auth = auth self.release_keeper = RaidenReleaseKeeper( data_path.joinpath('raiden_releases')) self.task_cache = {} self.task_storage = defaultdict(dict) self.scenario_name = os.path.basename( scenario_file.name).partition('.')[0] self.scenario = yaml.load(scenario_file) self.scenario_version = self.scenario.get('version', 1) if self.scenario_version not in SUPPORTED_SCENARIO_VERSIONS: raise ScenarioError( f'Unexpected scenario version {self.scenario_version}') self.data_path = data_path.joinpath('scenarios', self.scenario_name) self.data_path.mkdir(exist_ok=True, parents=True) log.debug('Data path', path=self.data_path) self.run_number = 0 run_number_file = self.data_path.joinpath('run_number.txt') if run_number_file.exists(): self.run_number = int(run_number_file.read_text()) + 1 run_number_file.write_text(str(self.run_number)) log.info('Run number', run_number=self.run_number) nodes = self.scenario['nodes'] node_mode = NodeMode.EXTERNAL.name if self.is_v2: node_mode = nodes.get('mode', '').upper() if not node_mode: raise ScenarioError( 'Version 2 scenarios require a "mode" in the "nodes" section.' ) try: self.node_mode = NodeMode[node_mode] except KeyError: known_modes = ', '.join(mode.name.lower() for mode in NodeMode) raise ScenarioError( f'Unknown node mode "{node_mode}". Expected one of {known_modes}', ) from None if self.is_managed: self.node_controller = NodeController( self, nodes.get('raiden_version', 'LATEST'), nodes['count'], nodes.get('default_options', {}), nodes.get('node_options', {}), ) else: if 'range' in nodes: range_config = nodes['range'] template = range_config['template'] self.raiden_nodes = [ template.format(i) for i in range(range_config['first'], range_config['last'] + 1) ] else: self.raiden_nodes = nodes['list'] self.node_commands = nodes.get('commands', {}) settings = self.scenario.get('settings') if settings is None: settings = {} self.timeout = settings.get('timeout', TIMEOUT) if self.is_managed: self.protocol = 'http' if 'protocol' in settings: log.warning( 'The "protocol" setting is not supported in "managed" node mode.' ) else: self.protocol = settings.get('protocol', 'http') self.notification_email = settings.get('notify') self.chain_name = settings.get('chain', 'any') if self.chain_name == 'any': self.chain_name = random.choice(list(chain_urls.keys())) elif self.chain_name not in chain_urls: raise ScenarioError( f'The scenario requested chain "{self.chain_name}" for which no RPC-URL is known.', ) log.info('Using chain', chain=self.chain_name) self.eth_rpc_urls = chain_urls[self.chain_name] self.client = JSONRPCClient( Web3(HTTPProvider(chain_urls[self.chain_name][0])), privkey=account.privkey, gas_price_strategy=get_gas_price_strategy( settings.get('gas_price', 'fast')), ) self.chain_id = self.client.web3.net.version balance = self.client.balance(account.address) if balance < OWN_ACCOUNT_BALANCE_MIN: raise ScenarioError( f'Insufficient balance ({balance / 10 ** 18} Eth) ' f'in account {to_checksum_address(account.address)} on chain "{self.chain_name}"', ) self.session = Session() # WTF? https://github.com/requests/requests/issues/2605 if auth: self.session.auth = tuple(auth.split(":")) self.session.mount('http', TimeOutHTTPAdapter(timeout=self.timeout)) self.session.mount('https', TimeOutHTTPAdapter(timeout=self.timeout)) self._node_to_address = None self.token_address = None scenario_config = self.scenario.get('scenario') if not scenario_config: raise ScenarioError( "Invalid scenario definition. Missing 'scenario' key.") try: (root_task_type, root_task_config), = scenario_config.items() except ValueError: # will be thrown if it's not a 1-element dict raise ScenarioError( "Invalid scenario definition. " "Exactly one root task is required below the 'scenario' key.", ) from None task_class = get_task_class_for_type(root_task_type) self.root_task = task_class(runner=self, config=root_task_config) def run_scenario(self): fund_tx = [] node_starter: gevent.Greenlet = None if self.is_managed: self.node_controller.initialize_nodes() node_addresses = self.node_controller.addresses node_balances = { address: self.client.balance(address) for address in node_addresses } low_balances = { address: balance for address, balance in node_balances.items() if balance < NODE_ACCOUNT_BALANCE_MIN } if low_balances: log.info('Funding nodes', nodes=low_balances.keys()) fund_tx = [ self.client.send_transaction( to=address, startgas=21000, value=NODE_ACCOUNT_BALANCE_FUND - balance, ) for address, balance in low_balances.items() ] node_starter = self.node_controller.start(wait=False) else: log.info("Fetching node addresses") unreachable_nodes = [ node for node, addr in self.node_to_address.items() if not addr ] if not self.node_to_address or unreachable_nodes: raise NodesUnreachableError( f"Raiden nodes unreachable: {','.join(unreachable_nodes)}", ) token_ctr = get_or_deploy_token(self) token_address = self.token_address = to_checksum_address( token_ctr.contract_address) first_node = self.get_node_baseurl(0) token_settings = self.scenario.get('token') or {} token_balance_min = token_settings.get( 'balance_min', DEFAULT_TOKEN_BALANCE_MIN, ) token_balance_fund = token_settings.get( 'balance_fund', DEFAULT_TOKEN_BALANCE_FUND, ) mint_tx = [] if self.is_managed: addresses = self.node_controller.addresses else: addresses = self.node_to_address.values() for address in addresses: balance = token_ctr.contract.functions.balanceOf(address).call() if balance < token_balance_min: mint_amount = token_balance_fund - balance startgas = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL log.debug("Minting tokens for", address=address, amount=mint_amount) mint_tx.append( token_ctr.transact('mintFor', startgas, mint_amount, address)) elif balance > token_balance_min: log.warning("Node is overfunded", address=address, balance=balance) wait_for_txs(self.client, mint_tx + fund_tx) if node_starter is not None: log.debug('Waiting for nodes to finish starting') node_starter.get(block=True) registered_tokens = set( self.session.get( API_URL_TOKENS.format(protocol=self.protocol, target_host=first_node), ).json(), ) if token_address not in registered_tokens: code, msg = self.register_token(token_address, first_node) if not 199 < code < 300: log.error("Couldn't register token with network", code=code, message=msg) raise TokenRegistrationError(msg) # Start root task root_task_greenlet = gevent.spawn(self.root_task) greenlets = [root_task_greenlet] if self.is_managed: greenlets.append(self.node_controller.start_node_monitor()) try: gevent.joinall(greenlets, raise_error=True) except BaseException: if not root_task_greenlet.dead: # Make sure we kill the tasks if a node dies root_task_greenlet.kill() raise def register_token(self, token_address, node): try: base_url = API_URL_TOKENS.format(protocol=self.protocol, target_host=node) url = "{}/{}".format(base_url, token_address) log.info("Registering token with network", url=url) resp = self.session.put(url) code = resp.status_code msg = resp.text except RequestException as ex: code = -1 msg = str(ex) return code, msg def _spawn_and_wait(self, objects, callback): tasks = {obj: gevent.spawn(callback, obj) for obj in objects} gevent.joinall(tasks.values()) return {obj: task.get() for obj, task in tasks.items()} @property def is_v2(self): return self.scenario_version == 2 @property def is_managed(self): return self.node_mode is NodeMode.MANAGED def get_node_address(self, index): if self.is_managed: return self.node_controller[index].address else: return self.node_to_address[self.raiden_nodes[index]] def get_node_baseurl(self, index): if self.is_managed: return self.node_controller[index].base_url else: return self.raiden_nodes[index] # Legacy for 'external' nodes def _get_node_addresses(self, nodes): def cb(node): log.debug("Getting node address", node=node) url = API_URL_ADDRESS.format(protocol=self.protocol, target_host=node) log.debug("Requesting", url=url) try: resp = self.session.get(url) except RequestException: log.error("Error fetching node address", url=url, node=node) return try: return resp.json().get('our_address', '') except ValueError: log.error( "Error decoding response", response=resp.text, code=resp.status_code, url=url, ) ret = self._spawn_and_wait(nodes, cb) return ret @property def node_to_address(self): if not self.raiden_nodes: return {} if self._node_to_address is None: self._node_to_address = { node: address for node, address in self._get_node_addresses( self.raiden_nodes).items() } return self._node_to_address
class ScenarioRunner: # TODO: #73 Drop support for version 1 scenario files. def __init__( self, account: Account, chain_urls: Dict[str, List[str]], auth: str, data_path: Path, scenario_file: Path, task_state_callback: Optional[Callable[ ["ScenarioRunner", Task, TaskState], None]] = None, ): from scenario_player.node_support import RaidenReleaseKeeper, NodeController self.task_count = 0 self.running_task_count = 0 self.auth = auth self.release_keeper = RaidenReleaseKeeper( data_path.joinpath("raiden_releases")) self.task_cache = {} self.task_state_callback = task_state_callback # Storage for arbitrary data tasks might need to persist self.task_storage = defaultdict(dict) self.scenario = Scenario(scenario_file) self.scenario_name = self.scenario.name self.data_path = data_path.joinpath("scenarios", self.scenario.name) self.data_path.mkdir(exist_ok=True, parents=True) log.debug("Data path", path=self.data_path) self.run_number = self.determine_run_number() self.node_mode = self.scenario.nodes.mode if self.is_managed: self.node_controller = NodeController( self, self.scenario.nodes.raiden_version, self.scenario.nodes.count, self.scenario.nodes.default_options, self.scenario.nodes.node_options, ) else: self.raiden_nodes = self.scenario.nodes self.node_commands = self.scenario.nodes.commands self.timeout = self.scenario.timeout self.protocol = self.scenario.protocol self.notification_email = self.scenario.notification_email self.chain_name, chain_urls = self.select_chain(chain_urls) self.eth_rpc_urls = chain_urls self.client = JSONRPCClient( Web3(HTTPProvider(chain_urls[0])), privkey=account.privkey, gas_price_strategy=self.scenario.gas_price_strategy, ) self.chain_id = int(self.client.web3.net.version) self.contract_manager = ContractManager(contracts_precompiled_path()) balance = self.client.balance(account.address) if balance < OWN_ACCOUNT_BALANCE_MIN: raise ScenarioError( f"Insufficient balance ({balance / 10 ** 18} Eth) " f'in account {to_checksum_address(account.address)} on chain "{self.chain_name}"' ) self.session = Session() if auth: self.session.auth = tuple(auth.split(":")) self.session.mount("http", TimeOutHTTPAdapter(timeout=self.timeout)) self.session.mount("https", TimeOutHTTPAdapter(timeout=self.timeout)) self._node_to_address = None self.token_address = None self.token_deployment_block = 0 self.token_network_address = None task_config = self.scenario.task_config task_class = self.scenario.task_class self.root_task = task_class(runner=self, config=task_config) def determine_run_number(self) -> int: """Determine the current run number. We check for a run number file, and use any number that is logged there after incrementing it. REFAC: Replace this with a property. """ run_number = 0 run_number_file = self.data_path.joinpath("run_number.txt") if run_number_file.exists(): run_number = int(run_number_file.read_text()) + 1 run_number_file.write_text(str(run_number)) log.info("Run number", run_number=run_number) return run_number def select_chain( self, chain_urls: Dict[str, List[str]]) -> Tuple[str, List[str]]: """Select a chain and return its name and RPC URL. If the currently loaded scenario's designated chain is set to 'any', we randomly select a chain from the given `chain_urls`. Otherwise, we will return `ScenarioRunner.scenario.chain_name` and whatever value may be associated with this key in `chain_urls`. :raises ScenarioError: if ScenarioRunner.scenario.chain_name is not one of `('any', 'Any', 'ANY')` and it is not a key in `chain_urls`. """ chain_name = self.scenario.chain_name if chain_name in ("any", "Any", "ANY"): chain_name = random.choice(list(chain_urls.keys())) log.info("Using chain", chain=chain_name) try: return chain_name, chain_urls[chain_name] except KeyError: raise ScenarioError( f'The scenario requested chain "{chain_name}" for which no RPC-URL is known.' ) def run_scenario(self): mint_gas = GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL * 2 fund_tx, node_starter, node_addresses, node_count = self._initialize_nodes( ) ud_token_tx, udc_ctr, should_deposit_ud_token = self._initialize_udc( gas_limit=mint_gas, node_count=node_count) mint_tx = self._initialize_scenario_token( node_addresses=node_addresses, udc_ctr=udc_ctr, should_deposit_ud_token=should_deposit_ud_token, gas_limit=mint_gas, ) wait_for_txs(self.client, fund_tx | ud_token_tx | mint_tx) if node_starter is not None: log.debug("Waiting for nodes to finish starting") node_starter.get(block=True) first_node = self.get_node_baseurl(0) registered_tokens = set( self.session.get( API_URL_TOKENS.format(protocol=self.protocol, target_host=first_node)).json()) if self.token_address not in registered_tokens: for _ in range(5): code, msg = self.register_token(self.token_address, first_node) if 199 < code < 300: break gevent.sleep(1) else: log.error("Couldn't register token with network", code=code, message=msg) raise TokenRegistrationError(msg) # The nodes need some time to find the token, see # https://github.com/raiden-network/raiden/issues/3544 # FIXME: Add proper check via API log.info("Waiting till new network is found by nodes") while self.token_network_address is None: self.token_network_address = self.session.get( API_URL_TOKEN_NETWORK_ADDRESS.format( protocol=self.protocol, target_host=first_node, token_address=self.token_address, )).json() gevent.sleep(1) log.info("Received token network address", token_network_address=self.token_network_address) # Start root task root_task_greenlet = gevent.spawn(self.root_task) greenlets = {root_task_greenlet} if self.is_managed: greenlets.add(self.node_controller.start_node_monitor()) try: gevent.joinall(greenlets, raise_error=True) except BaseException: if not root_task_greenlet.dead: # Make sure we kill the tasks if a node dies root_task_greenlet.kill() raise def _initialize_scenario_token( self, node_addresses: Set[ChecksumAddress], udc_ctr: Optional[ContractProxy], should_deposit_ud_token: bool, gas_limit: int, ) -> Set[TransactionHash]: token_ctr, token_block = get_or_deploy_token(self) self.token_address = to_checksum_address(token_ctr.contract_address) self.token_deployment_block = token_block token_settings = self.scenario.get("token") or {} token_balance_min = token_settings.get("balance_min", DEFAULT_TOKEN_BALANCE_MIN) token_balance_fund = token_settings.get("balance_fund", DEFAULT_TOKEN_BALANCE_FUND) mint_tx = set() for address in node_addresses: tx = mint_token_if_balance_low( token_contract=token_ctr, target_address=address, min_balance=token_balance_min, fund_amount=token_balance_fund, gas_limit=gas_limit, mint_msg="Minting tokens for", ) if tx: mint_tx.add(tx) if not should_deposit_ud_token: continue ud_deposit_balance = udc_ctr.contract.functions.effectiveBalance( address).call() if ud_deposit_balance < DEFAULT_TOKEN_BALANCE_MIN // 2: deposit_amount = (DEFAULT_TOKEN_BALANCE_FUND // 2) - ud_deposit_balance log.debug("Depositing into UDC", address=address, amount=deposit_amount) mint_tx.add( udc_ctr.transact("deposit", gas_limit, address, DEFAULT_TOKEN_BALANCE_FUND // 2)) return mint_tx def _initialize_udc( self, gas_limit: int, node_count: int ) -> Tuple[Set[TransactionHash], Optional[ContractProxy], bool]: our_address = to_checksum_address(self.client.address) udc_settings = self.scenario.services.get("udc", {}) udc_enabled = udc_settings.get("enable") ud_token_tx = set() if not udc_enabled: return ud_token_tx, None, False udc_ctr, ud_token_ctr = get_udc_and_token(self) ud_token_address = to_checksum_address(ud_token_ctr.contract_address) udc_address = to_checksum_address(udc_ctr.contract_address) log.info("UDC enabled", contract_address=udc_address, token_address=ud_token_address) should_deposit_ud_token = udc_enabled and udc_settings.get( "token", {}).get("deposit", False) if should_deposit_ud_token: tx = mint_token_if_balance_low( token_contract=ud_token_ctr, target_address=our_address, min_balance=DEFAULT_TOKEN_BALANCE_FUND * node_count, fund_amount=DEFAULT_TOKEN_BALANCE_FUND * 10 * node_count, gas_limit=gas_limit, mint_msg="Minting UD tokens", no_action_msg="UD token balance sufficient", ) if tx: ud_token_tx.add(tx) udt_allowance = ud_token_ctr.contract.functions.allowance( our_address, udc_address).call() if udt_allowance < DEFAULT_TOKEN_BALANCE_FUND * node_count: allow_amount = (DEFAULT_TOKEN_BALANCE_FUND * 10 * node_count) - udt_allowance log.debug("Updating UD token allowance", allowance=allow_amount) ud_token_tx.add( ud_token_ctr.transact("approve", gas_limit, udc_address, allow_amount)) else: log.debug("UD token allowance sufficient", allowance=udt_allowance) return ud_token_tx, udc_ctr, should_deposit_ud_token def _initialize_nodes( self ) -> Tuple[Set[TransactionHash], gevent.Greenlet, Set[ChecksumAddress], int]: fund_tx = set() node_starter: gevent.Greenlet = None if self.is_managed: self.node_controller.initialize_nodes() node_addresses = self.node_controller.addresses node_count = len(self.node_controller) node_balances = { address: self.client.balance(address) for address in node_addresses } low_balances = { address: balance for address, balance in node_balances.items() if balance < NODE_ACCOUNT_BALANCE_MIN } if low_balances: log.info("Funding nodes", nodes=low_balances.keys()) fund_tx = { self.client.send_transaction( to=address, startgas=21_000, value=NODE_ACCOUNT_BALANCE_FUND - balance) for address, balance in low_balances.items() } node_starter = self.node_controller.start(wait=False) else: log.info("Fetching node addresses") unreachable_nodes = [ node for node, addr in self.node_to_address.items() if not addr ] if not self.node_to_address or unreachable_nodes: raise NodesUnreachableError( f"Raiden nodes unreachable: {','.join(unreachable_nodes)}") node_addresses = set(self.node_to_address.values()) node_count = len(self.node_to_address) return fund_tx, node_starter, node_addresses, node_count def task_state_changed(self, task: "Task", state: "TaskState"): if self.task_state_callback: self.task_state_callback(self, task, state) def register_token(self, token_address, node): try: base_url = API_URL_TOKENS.format(protocol=self.protocol, target_host=node) url = "{}/{}".format(base_url, token_address) log.info("Registering token with network", url=url) resp = self.session.put(url) code = resp.status_code msg = resp.text except RequestException as ex: code = -1 msg = str(ex) return code, msg @staticmethod def _spawn_and_wait(objects, callback): tasks = {obj: gevent.spawn(callback, obj) for obj in objects} gevent.joinall(set(tasks.values())) return {obj: task.get() for obj, task in tasks.items()} @property def is_managed(self): return self.node_mode is NodeMode.MANAGED def get_node_address(self, index): if self.is_managed: return self.node_controller[index].address else: return self.node_to_address[self.raiden_nodes[index]] def get_node_baseurl(self, index): if self.is_managed: return self.node_controller[index].base_url else: return self.raiden_nodes[index] # Legacy for 'external' nodes def _get_node_addresses(self, nodes): def cb(node): log.debug("Getting node address", node=node) url = API_URL_ADDRESS.format(protocol=self.protocol, target_host=node) log.debug("Requesting", url=url) try: resp = self.session.get(url) except RequestException: log.error("Error fetching node address", url=url, node=node) return None try: return to_checksum_address(resp.json().get("our_address", "")) except ValueError: log.error("Error decoding response", response=resp.text, code=resp.status_code, url=url) return None ret = self._spawn_and_wait(nodes, cb) return ret @property def node_to_address(self) -> Dict[str, ChecksumAddress]: if not self.raiden_nodes: return {} if self._node_to_address is None: self._node_to_address = { node: address for node, address in self._get_node_addresses( self.raiden_nodes).items() } return self._node_to_address