def get_pfs_info(url: str) -> PFSInfo: try: response = requests.get(f"{url}/api/v1/info", timeout=DEFAULT_HTTP_REQUEST_TIMEOUT) infos = get_response_json(response) return PFSInfo( url=url, price=infos["price_info"], chain_id=infos["network_info"]["chain_id"], token_network_registry_address=TokenNetworkRegistryAddress( to_canonical_address( infos["network_info"]["token_network_registry_address"])), user_deposit_address=Address( to_canonical_address( infos["network_info"]["user_deposit_address"])), payment_address=to_canonical_address(infos["payment_address"]), message=infos["message"], operator=infos["operator"], version=infos["version"], ) except requests.exceptions.RequestException as e: msg = "Selected Pathfinding Service did not respond" raise ServiceRequestFailed(msg) from e except (json.JSONDecodeError, requests.exceptions.RequestException, KeyError) as e: msg = "Selected Pathfinding Service returned unexpected reply" raise ServiceRequestFailed(msg) from e
def get_pfs_info(url: str) -> PFSInfo: try: response = session.get(f"{url}/api/v1/info") infos = get_response_json(response) matrix_server_info = urlparse(infos["matrix_server"]) return PFSInfo( url=url, price=infos["price_info"], chain_id=infos["network_info"]["chain_id"], token_network_registry_address=TokenNetworkRegistryAddress( to_canonical_address(infos["network_info"]["token_network_registry_address"]) ), user_deposit_address=Address( to_canonical_address(infos["network_info"]["user_deposit_address"]) ), payment_address=to_canonical_address(infos["payment_address"]), message=infos["message"], operator=infos["operator"], version=infos["version"], confirmed_block_number=infos["network_info"]["confirmed_block"]["number"], matrix_server=matrix_server_info.netloc, matrix_room_id=infos.get("matrix_room_id"), ) except RequestException as e: msg = "Selected Pathfinding Service did not respond" raise ServiceRequestFailed(msg) from e except (json.JSONDecodeError, KeyError, ValueError) as e: msg = "Selected Pathfinding Service returned unexpected reply" raise ServiceRequestFailed(msg) from e
def post_pfs_paths( url: str, token_network_address: TokenNetworkAddress, payload: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], UUID]: try: response = requests.post( f"{url}/api/v1/{to_checksum_address(token_network_address)}/paths", json=payload, timeout=DEFAULT_HTTP_REQUEST_TIMEOUT, ) except requests.RequestException as e: raise ServiceRequestFailed( f"Could not connect to Pathfinding Service ({str(e)})", dict(parameters=payload, exc_info=True), ) if response.status_code != 200: info = {"http_error": response.status_code} try: response_json = get_response_json(response) except ValueError: log.error( "Pathfinding Service returned error code (malformed json in response)", response=response, ) raise ServiceRequestFailed( "Pathfinding Service returned error code (malformed json in response)", info) error = info["error"] = response_json.get("errors") error_code = info["error_code"] = response_json.get("error_code", 0) if PFSError.is_iou_rejected(error_code): raise ServiceRequestIOURejected(error, error_code) raise ServiceRequestFailed(error, info) try: response_json = get_response_json(response) return response_json["result"], UUID(response_json["feedback_token"]) except KeyError: raise ServiceRequestFailed( "Answer from Pathfinding Service not understood ('result' field missing)", dict(response=get_response_json(response)), ) except ValueError: raise ServiceRequestFailed( "Pathfinding Service returned invalid json", dict(response_text=response.text, exc_info=True), )
def update_iou( iou: IOU, privkey: bytes, added_amount: TokenAmount = ZERO_TOKENS, expiration_block: Optional[BlockNumber] = None, ) -> IOU: expected_signature = Signature( sign_one_to_n_iou( privatekey=to_hex(privkey), sender=to_checksum_address(iou.sender), receiver=to_checksum_address(iou.receiver), amount=iou.amount, expiration_block=iou.expiration_block, one_to_n_address=to_checksum_address(iou.one_to_n_address), chain_id=iou.chain_id, )) if iou.signature != expected_signature: raise ServiceRequestFailed( "Last IOU as given by the Pathfinding Service is invalid (signature does not match)" ) iou.amount = TokenAmount(iou.amount + added_amount) if expiration_block: iou.expiration_block = expiration_block iou.sign(privkey) return iou
def update_iou( iou: typing.Dict[str, typing.Any], privkey: bytes, added_amount: typing.TokenAmount = 0, expiration_block: typing.Optional[typing.BlockNumber] = None, ) -> typing.Dict[str, typing.Any]: expected_signature = to_hex( sign_one_to_n_iou( privatekey=to_hex(privkey), expiration=iou['expiration_block'], sender=iou['sender'], receiver=iou['receiver'], amount=iou['amount'], )) if iou.get('signature') != expected_signature: raise ServiceRequestFailed( 'Last IOU as given by the pathfinding service is invalid (signature does not match)', ) iou['amount'] += added_amount if expiration_block: iou['expiration_block'] = expiration_block iou['signature'] = sign_one_to_n_iou( privatekey=to_hex(privkey), expiration=iou['expiration_block'], sender=iou['sender'], receiver=iou['receiver'], amount=iou['amount'], ) return iou
def get_last_iou( url: str, token_network_address: typing.Union[typing.TokenNetworkAddress, typing.TokenNetworkID], sender: typing.Address, receiver: typing.Address, ) -> typing.Optional[typing.Dict]: timestamp = datetime.utcnow().isoformat(timespec='seconds') signature = to_hex( eth_sign_hash_message( Web3.toBytes(hexstr=sender) + Web3.toBytes(hexstr=receiver) + bytes(timestamp, 'utf-8'), )) try: return requests.get( f'{url}/api/v1/{to_checksum_address(token_network_address)}/payment/iou', data=dict(sender=sender, receiver=receiver, timestamp=timestamp, signature=signature), timeout=DEFAULT_HTTP_REQUEST_TIMEOUT, ).json().get('last_iou') except (requests.exceptions.RequestException, ValueError) as e: raise ServiceRequestFailed(str(e))
def update_iou( iou: Dict[str, Any], privkey: bytes, added_amount: TokenAmount = ZERO_TOKENS, expiration_block: Optional[BlockNumber] = None, ) -> Dict[str, Any]: expected_signature = to_hex( sign_one_to_n_iou( privatekey=to_hex(privkey), expiration=iou["expiration_block"], sender=iou["sender"], receiver=iou["receiver"], amount=iou["amount"], )) if iou.get("signature") != expected_signature: raise ServiceRequestFailed( "Last IOU as given by the pathfinding service is invalid (signature does not match)" ) iou["amount"] += added_amount if expiration_block: iou["expiration_block"] = expiration_block iou["signature"] = to_hex( sign_one_to_n_iou( privatekey=to_hex(privkey), expiration=iou["expiration_block"], sender=iou["sender"], receiver=iou["receiver"], amount=iou["amount"], )) return iou
def get_last_iou( url: str, token_network_address: Union[TokenNetworkAddress, TokenNetworkID], sender: Address, receiver: Address, privkey: bytes, ) -> Optional[Dict]: timestamp = datetime.utcnow().isoformat(timespec="seconds") signature_data = sender + receiver + Web3.toBytes(text=timestamp) signature = to_hex(LocalSigner(privkey).sign(signature_data)) try: return (requests.get( f"{url}/api/v1/{to_checksum_address(token_network_address)}/payment/iou", params=dict( sender=to_checksum_address(sender), receiver=to_checksum_address(receiver), timestamp=timestamp, signature=signature, ), timeout=DEFAULT_HTTP_REQUEST_TIMEOUT, ).json().get("last_iou")) except (requests.exceptions.RequestException, ValueError) as e: raise ServiceRequestFailed(str(e))
def post_pfs_paths(url, token_network_address, payload): try: response = requests.post( f'{url}/api/v1/{to_checksum_address(token_network_address)}/paths', json=payload, timeout=DEFAULT_HTTP_REQUEST_TIMEOUT, ) except requests.RequestException as e: raise ServiceRequestFailed( f'Could not connect to Pathfinding Service ({str(e)})', dict(parameters=payload, exc_info=True), ) if response.status_code != 200: info = {'http_error': response.status_code} try: response_json = response.json() except ValueError: raise ServiceRequestFailed( 'Pathfinding service returned error code (malformed json in response)', info, ) else: error = info['error'] = response_json.get('errors') error_code = info['error_code'] = response_json.get( 'error_code', 0) if PFSError.is_iou_rejected(error_code): raise ServiceRequestIOURejected(error, error_code) raise ServiceRequestFailed('Pathfinding service returned error code', info) try: return response.json()['result'] except KeyError: raise ServiceRequestFailed( "Answer from pathfinding service not understood ('result' field missing)", dict(response=response.json()), ) except ValueError: raise ServiceRequestFailed( 'Pathfinding service returned invalid json', dict(response_text=response.text, exc_info=True), )
def post_pfs_paths( url: str, token_network_address: TokenNetworkAddress, payload: Dict[str, Any] ) -> Tuple[List[Dict[str, Any]], UUID]: try: response = session.post( f"{url}/api/v1/{to_checksum_address(token_network_address)}/paths", json=payload ) except RequestException as e: raise ServiceRequestFailed( f"Could not connect to Pathfinding Service ({str(e)})", dict(parameters=payload, exc_info=True), ) if response.status_code != 200: try: response_json = get_response_json(response) except ValueError: log.error( "Pathfinding Service returned error code (malformed json in response)", response=response, ) raise ServiceRequestFailed( "Pathfinding Service returned error code (malformed json in response)", {"http_error": response.status_code}, ) raise PFSReturnedError.from_response(response_json) try: response_json = get_response_json(response) return response_json["result"], UUID(response_json["feedback_token"]) except KeyError: raise ServiceRequestFailed( "Answer from Pathfinding Service not understood ('result' field missing)", dict(response=get_response_json(response)), ) except ValueError: raise ServiceRequestFailed( "Pathfinding Service returned invalid json", dict(response_text=response.text, exc_info=True), )
def get_last_iou( url: str, token_network_address: TokenNetworkAddress, sender: Address, receiver: Address, privkey: bytes, ) -> Optional[IOU]: timestamp = datetime.utcnow().isoformat(timespec="seconds") signature_data = sender + receiver + Web3.toBytes(text=timestamp) signature = to_hex(LocalSigner(privkey).sign(signature_data)) try: response = requests.get( f"{url}/api/v1/{to_checksum_address(token_network_address)}/payment/iou", params=dict( sender=to_checksum_address(sender), receiver=to_checksum_address(receiver), timestamp=timestamp, signature=signature, ), timeout=DEFAULT_HTTP_REQUEST_TIMEOUT, ) data = json.loads(response.content).get("last_iou") if data is None: return None return IOU( sender=to_canonical_address(data["sender"]), receiver=to_canonical_address(data["receiver"]), one_to_n_address=OneToNAddress( to_canonical_address(data["one_to_n_address"])), amount=data["amount"], expiration_block=data["expiration_block"], chain_id=data["chain_id"], signature=Signature(decode_hex(data["signature"])), ) except (requests.exceptions.RequestException, ValueError, KeyError) as e: raise ServiceRequestFailed(str(e))
def get_pfs_info(url: str) -> PFSInfo: try: response = requests.get(f"{url}/api/v1/info", timeout=DEFAULT_HTTP_REQUEST_TIMEOUT) infos = get_response_json(response) return PFSInfo( url=url, price=infos["price_info"], chain_id=infos["network_info"]["chain_id"], token_network_registry_address=TokenNetworkRegistryAddress( to_canonical_address( infos["network_info"]["registry_address"])), payment_address=to_canonical_address(infos["payment_address"]), message=infos["message"], operator=infos["operator"], version=infos["version"], ) except (json.JSONDecodeError, requests.exceptions.RequestException, KeyError) as e: raise ServiceRequestFailed(str(e)) from e
def query_paths( pfs_config: PFSConfig, our_address: Address, privkey: bytes, current_block_number: BlockNumber, token_network_address: TokenNetworkAddress, one_to_n_address: OneToNAddress, chain_id: ChainID, route_from: InitiatorAddress, route_to: TargetAddress, value: PaymentAmount, ) -> Tuple[List[Dict[str, Any]], Optional[UUID]]: """ Query paths from the PFS. Send a request to the /paths endpoint of the PFS specified in service_config, and retry in case of a failed request if it makes sense. """ payload = { "from": to_checksum_address(route_from), "to": to_checksum_address(route_to), "value": value, "max_paths": pfs_config.max_paths, } offered_fee = pfs_config.info.price scrap_existing_iou = False for retries in reversed(range(MAX_PATHS_QUERY_ATTEMPTS)): # Since the IOU amount is monotonically increasing, only a single # greenlet can be in charge of increasing it at the same time. Using a # semaphore in this primitive way limits the speed at which we can do # simultaneous transfers. # See https://github.com/raiden-network/raiden/issues/5647 with gevent.lock.Semaphore(): if offered_fee > 0: new_iou = create_current_iou( pfs_config=pfs_config, token_network_address=token_network_address, one_to_n_address=one_to_n_address, our_address=our_address, privkey=privkey, chain_id=chain_id, block_number=current_block_number, offered_fee=offered_fee, scrap_existing_iou=scrap_existing_iou, ) payload["iou"] = new_iou.as_json() log.info( "Requesting paths from Pathfinding Service", url=pfs_config.info.url, token_network_address=to_checksum_address( token_network_address), payload=payload, ) try: return post_pfs_paths( url=pfs_config.info.url, token_network_address=token_network_address, payload=payload, ) except ServiceRequestIOURejected as error: code = error.error_code log.debug("Pathfinding Service rejected IOU", error=error) if retries == 0 or code in ( PFSError.WRONG_IOU_RECIPIENT, PFSError.DEPOSIT_TOO_LOW, ): raise elif code in (PFSError.IOU_ALREADY_CLAIMED, PFSError.IOU_EXPIRED_TOO_EARLY): scrap_existing_iou = True elif code == PFSError.INSUFFICIENT_SERVICE_PAYMENT: try: new_info = get_pfs_info(pfs_config.info.url) except ServiceRequestFailed: raise ServiceRequestFailed( "Could not get updated fee information from Pathfinding Service." ) if new_info.price > pfs_config.maximum_fee: raise ServiceRequestFailed("PFS fees too high.") log.info(f"Pathfinding Service increased fees", new_price=new_info.price) pfs_config.info = new_info elif code == PFSError.NO_ROUTE_FOUND: log.info( f"Pathfinding Service can not find a route: {error}.") return list(), None log.info( f"Pathfinding Service rejected our payment. Reason: {error}. Attempting again." ) # If we got no results after MAX_PATHS_QUERY_ATTEMPTS return empty list of paths return list(), None
def query_paths( pfs_config: PFSConfig, our_address: Address, privkey: bytes, current_block_number: BlockNumber, token_network_address: TokenNetworkAddress, one_to_n_address: Address, chain_id: ChainID, route_from: InitiatorAddress, route_to: TargetAddress, value: PaymentAmount, ) -> Tuple[List[Dict[str, Any]], Optional[UUID]]: """ Query paths from the PFS. Send a request to the /paths endpoint of the PFS specified in service_config, and retry in case of a failed request if it makes sense. """ payload = { "from": to_checksum_address(route_from), "to": to_checksum_address(route_to), "value": value, "max_paths": pfs_config.max_paths, } offered_fee = pfs_config.info.price scrap_existing_iou = False for retries in reversed(range(MAX_PATHS_QUERY_ATTEMPTS)): if offered_fee > 0: new_iou = create_current_iou( pfs_config=pfs_config, token_network_address=token_network_address, one_to_n_address=one_to_n_address, our_address=our_address, privkey=privkey, chain_id=chain_id, block_number=current_block_number, offered_fee=offered_fee, scrap_existing_iou=scrap_existing_iou, ) payload["iou"] = new_iou.as_json() log.info( "Requesting paths from Pathfinding Service", url=pfs_config.info.url, token_network_address=token_network_address, payload=payload, ) try: return post_pfs_paths( url=pfs_config.info.url, token_network_address=token_network_address, payload=payload, ) except ServiceRequestIOURejected as error: code = error.error_code if retries == 0 or code in (PFSError.WRONG_IOU_RECIPIENT, PFSError.DEPOSIT_TOO_LOW): raise elif code in (PFSError.IOU_ALREADY_CLAIMED, PFSError.IOU_EXPIRED_TOO_EARLY): scrap_existing_iou = True elif code == PFSError.INSUFFICIENT_SERVICE_PAYMENT: try: new_info = get_pfs_info(pfs_config.info.url) except ServiceRequestFailed: raise ServiceRequestFailed( "Could not get updated fees from PFS.") if new_info.price > pfs_config.maximum_fee: raise ServiceRequestFailed("PFS fees too high.") log.info(f"PFS increased fees", new_price=new_info.price) pfs_config.info = new_info elif code == PFSError.NO_ROUTE_FOUND: log.info(f"PFS cannot find a route: {error}.") return list(), None log.info( f"PFS rejected our IOU, reason: {error}. Attempting again.") # If we got no results after MAX_PATHS_QUERY_ATTEMPTS return empty list of paths return list(), None