def set_url(self, url: str) -> None: """Sets the url needed to access the service via HTTP for the caller""" log_details: Dict[str, Any] = { "node": to_checksum_address(self.node_address), "contract": to_checksum_address(self.address), "url": url, } if not url.strip(): msg = "Invalid empty URL" raise BrokenPreconditionError(msg) parsed_url = urlparse(url) if parsed_url.scheme not in ("http", "https"): msg = "URL provided to service registry must be a valid HTTP(S) endpoint." raise BrokenPreconditionError(msg) with log_transaction(log, "set_url", log_details): gas_limit = self.proxy.estimate_gas("latest", "setURL", url) if not gas_limit: msg = f"URL {url} is invalid" raise RaidenUnrecoverableError(msg) log_details["gas_limit"] = gas_limit transaction_hash = self.proxy.transact("setURL", gas_limit, url) receipt = self.client.poll(transaction_hash) failed_receipt = check_transaction_threw(receipt=receipt) if failed_receipt: msg = f"URL {url} is invalid" raise RaidenUnrecoverableError(msg)
def init( self, monitoring_service_address: MonitoringServiceAddress, one_to_n_address: OneToNAddress, given_block_identifier: BlockSpecification, ) -> None: """ Initialize the UserDeposit contract with MS and OneToN addresses """ log_details = { "monitoring_service_address": to_checksum_address(monitoring_service_address), "one_to_n_address": to_checksum_address(one_to_n_address), } check_address_has_code( client=self.client, address=Address(monitoring_service_address), contract_name=CONTRACT_MONITORING_SERVICE, expected_code=decode_hex( self.contract_manager.get_runtime_hexcode( CONTRACT_MONITORING_SERVICE)), ) check_address_has_code( client=self.client, address=Address(one_to_n_address), contract_name=CONTRACT_ONE_TO_N, expected_code=decode_hex( self.contract_manager.get_runtime_hexcode(CONTRACT_ONE_TO_N)), ) try: existing_monitoring_service_address = self.monitoring_service_address( block_identifier=given_block_identifier) existing_one_to_n_address = self.one_to_n_address( block_identifier=given_block_identifier) except ValueError: pass except BadFunctionCallOutput: raise_on_call_returned_empty(given_block_identifier) else: if existing_monitoring_service_address != EMPTY_ADDRESS: msg = ( f"MonitoringService contract address is already set to " f"{to_checksum_address(existing_monitoring_service_address)}" ) raise BrokenPreconditionError(msg) if existing_one_to_n_address != EMPTY_ADDRESS: msg = (f"OneToN contract address is already set to " f"{to_checksum_address(existing_one_to_n_address)}") raise BrokenPreconditionError(msg) with log_transaction(log, "init", log_details): self._init( monitoring_service_address=monitoring_service_address, one_to_n_address=one_to_n_address, log_details=log_details, )
def deposit( self, beneficiary: Address, total_deposit: TokenAmount, given_block_identifier: BlockSpecification, ) -> None: """ Deposit provided amount into the user-deposit contract to the beneficiary's account. """ token_address = self.token_address(given_block_identifier) token = self.proxy_manager.token(token_address=token_address) log_details = { "beneficiary": to_checksum_address(beneficiary), "contract": to_checksum_address(self.address), "node": to_checksum_address(self.node_address), "total_deposit": total_deposit, } # To prevent concurrent transactions for token transfers where it is unknown if # we have enough capacity for both, we acquire the lock # for the token proxy. Example: A user deposit and a channel deposit # for the same token. with self.deposit_lock, token.token_lock: # check preconditions try: previous_total_deposit = self.get_total_deposit( address=beneficiary, block_identifier=given_block_identifier) current_balance = token.balance_of( address=self.node_address, block_identifier=given_block_identifier) whole_balance = self.whole_balance( block_identifier=given_block_identifier) whole_balance_limit = self.whole_balance_limit( block_identifier=given_block_identifier) except ValueError: # If 'given_block_identifier' has been pruned, we cannot perform the # precondition checks but must still set the amount_to_deposit to a # reasonable value. previous_total_deposit = self.get_total_deposit( address=beneficiary, block_identifier=self.client.get_checking_block()) amount_to_deposit = TokenAmount(total_deposit - previous_total_deposit) except BadFunctionCallOutput: raise_on_call_returned_empty(given_block_identifier) else: log_details["previous_total_deposit"] = previous_total_deposit amount_to_deposit = TokenAmount(total_deposit - previous_total_deposit) if whole_balance + amount_to_deposit > UINT256_MAX: msg = ( f"Current whole balance is {whole_balance}. " f"The new deposit of {amount_to_deposit} would lead to an overflow." ) raise BrokenPreconditionError(msg) if whole_balance + amount_to_deposit > whole_balance_limit: msg = ( f"Current whole balance is {whole_balance}. " f"With the new deposit of {amount_to_deposit}, the deposit " f"limit of {whole_balance_limit} would be exceeded.") raise BrokenPreconditionError(msg) if total_deposit <= previous_total_deposit: msg = ( f"Current total deposit {previous_total_deposit} is already larger " f"than the requested total deposit amount {total_deposit}" ) raise BrokenPreconditionError(msg) if current_balance < amount_to_deposit: msg = ( f"new_total_deposit - previous_total_deposit = {amount_to_deposit} " f"can not be larger than the available balance {current_balance}, " f"for token at address {to_checksum_address(token.address)}" ) raise BrokenPreconditionError(msg) with log_transaction(log, "deposit", log_details): self._deposit( beneficiary=beneficiary, token=token, total_deposit=total_deposit, amount_to_deposit=amount_to_deposit, log_details=log_details, )
def register_secret_batch(self, secrets: List[Secret]) -> None: """Register a batch of secrets. Check if they are already registered at the given block identifier.""" secrets_to_register = list() secrethashes_to_register = list() secrethashes_not_sent = list() transaction_result = AsyncResult() wait_for = set() # secret registration has no preconditions: # # - The action does not depend on any state, it's always valid to call # it. # - This action is always susceptible to race conditions. # # Therefore this proxy only needs to detect if the secret is already # registered, to avoid sending obviously unecessary transactions, and # it has to handle race conditions. with self._open_secret_transactions_lock: verification_block_hash = self.client.get_confirmed_blockhash() for secret in secrets: secrethash = sha256_secrethash(secret) secrethash_hex = encode_hex(secrethash) # Do the local test on `open_secret_transactions` first, then # if necessary do an RPC call. # # The call to `is_secret_registered` has two conflicting # requirements: # # - Avoid sending duplicated transactions for the same lock # - Operating on a consistent/confirmed view of the blockchain # (if a secret has been registered in a block that is not # confirmed it doesn't count yet, an optimization would be to # *not* send the transaction and wait for the confirmation) # # The code below respects the consistent blockchain view, # meaning that if this proxy method is called with an old # blockhash an unecessary transaction will be sent, and the # error will be treated as a race-condition. other_result = self.open_secret_transactions.get(secret) if other_result is not None: wait_for.add(other_result) secrethashes_not_sent.append(secrethash_hex) elif not self.is_secret_registered(secrethash, verification_block_hash): secrets_to_register.append(secret) secrethashes_to_register.append(secrethash_hex) self.open_secret_transactions[secret] = transaction_result # From here on the lock is not required. Context-switches will happen # for the gas estimation and the transaction, however the # synchronization data is limited to the open_secret_transactions log_details = { "node": to_checksum_address(self.node_address), "contract": to_checksum_address(self.address), "secrethashes": secrethashes_to_register, "secrethashes_not_sent": secrethashes_not_sent, } with log_transaction(log, "register_secret_batch", log_details): if secrets_to_register: self._register_secret_batch(secrets_to_register, transaction_result, log_details) gevent.joinall(wait_for, raise_error=True)
def add_token( self, token_address: TokenAddress, channel_participant_deposit_limit: TokenAmount, token_network_deposit_limit: TokenAmount, block_identifier: BlockSpecification, ) -> TokenNetworkAddress: """ Register token of `token_address` with the token network. The limits apply for version 0.13.0 and above of raiden-contracts, since instantiation also takes the limits as constructor arguments. """ if block_identifier == "latest": raise ValueError( 'Calling a proxy with "latest" is usually wrong because ' "the result of the precondition check is not precisely predictable." ) if token_address == NULL_ADDRESS_BYTES: raise InvalidTokenAddress( "The call to register a token at 0x00..00 will fail.") if token_network_deposit_limit <= 0: raise InvalidTokenNetworkDepositLimit( f"Token network deposit limit of {token_network_deposit_limit} is invalid" ) if channel_participant_deposit_limit > token_network_deposit_limit: raise InvalidChannelParticipantDepositLimit( f"Channel participant deposit limit of " f"{channel_participant_deposit_limit} is invalid") token_proxy = self.proxy_manager.token(token_address) try: token_supply = token_proxy.total_supply( block_identifier=block_identifier) already_registered = self.get_token_network( token_address=token_address, block_identifier=block_identifier) deprecation_executor = self.get_deprecation_executor( block_identifier=block_identifier) settlement_timeout_min = self.settlement_timeout_min( block_identifier=block_identifier) settlement_timeout_max = self.settlement_timeout_max( block_identifier=block_identifier) chain_id = self.get_chain_id(block_identifier=block_identifier) secret_registry_address = self.get_secret_registry_address( block_identifier=block_identifier) max_token_networks = self.get_max_token_networks( block_identifier=block_identifier) token_networks_created = self.get_token_network_created( block_identifier=block_identifier) except ValueError: # If `block_identifier` has been pruned the checks cannot be performed pass except BadFunctionCallOutput: raise_on_call_returned_empty(block_identifier) else: if token_networks_created >= max_token_networks: raise BrokenPreconditionError( f"Number of token networks will exceed the max of {max_token_networks}" ) if token_supply == "": raise InvalidToken("Given token address does not follow the " "ERC20 standard (missing `totalSupply()`)") if already_registered: raise BrokenPreconditionError( "The token is already registered in the TokenNetworkRegistry." ) if deprecation_executor == NULL_ADDRESS_BYTES: raise BrokenPreconditionError( "The deprecation executor property for the TokenNetworkRegistry is invalid." ) if chain_id == 0: raise BrokenPreconditionError( "The chain ID property for the TokenNetworkRegistry is invalid." ) if secret_registry_address == NULL_ADDRESS_BYTES: raise BrokenPreconditionError( "The secret registry address for the token network is invalid." ) if settlement_timeout_min == 0: raise BrokenPreconditionError( "The minimum settlement timeout for the token network " "should be larger than zero.") if settlement_timeout_min == 0: raise BrokenPreconditionError( "The minimum settlement timeout for the token network " "should be larger than zero.") if settlement_timeout_max <= settlement_timeout_min: raise BrokenPreconditionError( "The maximum settlement timeout for the token network " "should be larger than the minimum settlement timeout.") log_details = { "node": to_checksum_address(self.node_address), "contract": to_checksum_address(self.address), "token_address": to_checksum_address(token_address), } with log_transaction(log, "add_token", log_details): return self._add_token( token_address=token_address, channel_participant_deposit_limit= channel_participant_deposit_limit, token_network_deposit_limit=token_network_deposit_limit, log_details=log_details, )
def approve(self, allowed_address: Address, allowance: TokenAmount) -> None: """ Approve `allowed_address` to transfer up to `deposit` amount of token. Note: For channel deposit please use the channel proxy, since it does additional validations. We assume there to be sufficient balance as a precondition if this is called, so it is not checked as a precondition here. """ # Note that given_block_identifier is not used here as there # are no preconditions to check before sending the transaction # There are no direct calls to this method in any event handler, # so a precondition check would make no sense. with self.token_lock: log_details = { "node": to_checksum_address(self.node_address), "contract": to_checksum_address(self.address), "allowed_address": to_checksum_address(allowed_address), "allowance": allowance, } with log_transaction(log, "approve", log_details): checking_block = self.client.get_checking_block() error_prefix = "Call to approve will fail" gas_limit = self.proxy.estimate_gas( checking_block, "approve", to_checksum_address(allowed_address), allowance) if gas_limit: error_prefix = "Call to approve failed" gas_limit = safe_gas_limit(gas_limit) log_details["gas_limit"] = gas_limit transaction_hash = self.proxy.transact( "approve", gas_limit, to_checksum_address(allowed_address), allowance) receipt = self.client.poll(transaction_hash) failed_receipt = check_transaction_threw(receipt=receipt) if failed_receipt: failed_at_blockhash = encode_hex( failed_receipt["blockHash"]) if failed_receipt["cumulativeGasUsed"] == gas_limit: msg = ( f"approve failed and all gas was used ({gas_limit}). " f"Estimate gas may have underestimated approve, or " f"succeeded even though an assert is triggered, or " f"the smart contract code has a conditional assert." ) raise RaidenRecoverableError(msg) balance = self.balance_of(self.client.address, failed_at_blockhash) if balance < allowance: msg = ( f"{error_prefix} Your balance of {balance} is " "below the required amount of {allowance}.") if balance == 0: msg += ( " Note: The balance was 0, which may also happen " "if the contract is not a valid ERC20 token " "(balanceOf method missing).") raise RaidenRecoverableError(msg) raise RaidenRecoverableError( f"{error_prefix}. The reason is unknown, you have enough tokens for " f"the requested allowance and enough eth to pay the gas. There may " f"be a problem with the token contract.") else: failed_at = self.proxy.rpc_client.get_block("latest") failed_at_blockhash = encode_hex(failed_at["hash"]) failed_at_blocknumber = failed_at["number"] self.proxy.rpc_client.check_for_insufficient_eth( transaction_name="approve", transaction_executed=False, required_gas=GAS_REQUIRED_FOR_APPROVE, block_identifier=failed_at_blocknumber, ) balance = self.balance_of(self.client.address, failed_at_blockhash) if balance < allowance: msg = (f"{error_prefix} Your balance of {balance} is " "below the required amount of {allowance}.") if balance == 0: msg += ( " Note: The balance was 0, which may also happen if the contract " "is not a valid ERC20 token (balanceOf method missing)." ) raise RaidenRecoverableError(msg) raise RaidenRecoverableError( f"{error_prefix} Gas estimation failed for unknown reason. " f"Please make sure the contract is a valid ERC20 token." )
def transfer(self, to_address: Address, amount: TokenAmount) -> None: """ Transfer `amount` tokens to `to_address`. Note: We assume there to be sufficient balance as a precondition if this is called, so that is not checked as a precondition here. """ # Note that given_block_identifier is not used here as there # are no preconditions to check before sending the transaction # There are no direct calls to this method in any event handler, # so a precondition check would make no sense. with self.token_lock: log_details = { "node": to_checksum_address(self.node_address), "contract": to_checksum_address(self.address), "to_address": to_checksum_address(to_address), "amount": amount, } with log_transaction(log, "transfer", log_details): checking_block = self.client.get_checking_block() gas_limit = self.proxy.estimate_gas( checking_block, "transfer", to_checksum_address(to_address), amount) failed_receipt = None if gas_limit is not None: gas_limit = safe_gas_limit(gas_limit) log_details["gas_limit"] = gas_limit transaction_hash = self.proxy.transact( "transfer", gas_limit, to_checksum_address(to_address), amount) receipt = self.client.poll(transaction_hash) # TODO: check Transfer event (issue: #2598) failed_receipt = check_transaction_threw(receipt=receipt) if gas_limit is None or failed_receipt is not None: if failed_receipt: failed_at_number = failed_receipt["blockNumber"] else: failed_at_number = checking_block failed_at_hash = encode_hex( self.client.blockhash_from_blocknumber( failed_at_number)) self.proxy.rpc_client.check_for_insufficient_eth( transaction_name="transfer", transaction_executed=False, required_gas=GAS_LIMIT_FOR_TOKEN_CONTRACT_CALL, block_identifier=failed_at_number, ) balance = self.balance_of(self.client.address, failed_at_hash) if balance < amount: msg = ( f"Call to transfer will fail. Your balance of {balance} is " f"below the required amount of {amount}.") if balance == 0: msg += ( " Note: The balance was 0, which may also happen if the contract " "is not a valid ERC20 token (balanceOf method missing)." ) raise RaidenRecoverableError(msg) if gas_limit is None: raise RaidenRecoverableError( "Call to transfer will fail. Gas estimation failed for unknown " "reason. Please make sure the contract is a valid ERC20 token." ) else: raise RaidenRecoverableError( "Call to transfer failed for unknown reason. Please make sure the " "contract is a valid ERC20 token.")