Exemplo n.º 1
0
class Session(requests.Session):
    def __init__(self,
                 client: Client = None,
                 endpoint_url: str = None,
                 retry_interval: float = 5,
                 initial_deposit: Callable[[int],
                                           int] = lambda price: 10 * price,
                 topup_deposit: Callable[[int], int] = lambda price: 5 * price,
                 close_channel_on_exit: bool = False,
                 **client_kwargs) -> None:
        requests.Session.__init__(self)
        self.channel = None  # type: Channel
        self.endpoint_url = endpoint_url
        self.client = client
        self.retry_interval = retry_interval
        self.initial_deposit = initial_deposit
        self.topup_deposit = topup_deposit
        self.close_channel_on_exit = close_channel_on_exit

        if self.client is None:
            self.client = Client(**client_kwargs)

    def close(self):
        if self.close_channel_on_exit and self.channel.state == Channel.State.open:
            self.close_channel()
        requests.Session.close(self)

    def request(self, method: str, url: str, **kwargs) -> Response:
        self.on_init(method, url, **kwargs)
        retry = True
        response = None
        while retry:
            response, retry = self._request_resource(method, url, **kwargs)

        self.on_exit(method, url, response, **kwargs)
        return response

    def close_channel(self, endpoint_url: str = None):
        if self.channel is None:
            log.debug('No channel to close.')
            return

        if endpoint_url is None:
            endpoint_url = self.endpoint_url

        if endpoint_url is None:
            log.warning(
                'No endpoint URL specified to request a closing signature.')
            self.on_cooperative_close_denied()
            return

        log.debug(
            'Requesting closing signature from server for balance {} on channel {}/{}/{}.'
            .format(self.channel.balance, self.channel.sender,
                    self.channel.sender, self.channel.block))
        url = '{}/api/1/channels/{}/{}'.format(endpoint_url,
                                               self.channel.sender,
                                               self.channel.block)

        # We need to call request directly because delete would perform a uRaiden request.
        try:
            response = requests.Session.request(
                self, 'DELETE', url, data={'balance': self.channel.balance})
        except requests.exceptions.ConnectionError as err:
            log.error(
                'Could not get a response from the server while requesting a closing signature: {}'
                .format(err))
            response = None

        failed = True
        if response is not None and response.status_code == requests.codes.OK:
            closing_sig = response.json()['close_signature']
            failed = self.channel.close_cooperatively(
                decode_hex(closing_sig)) is None

        if response is None or failed:
            self.on_cooperative_close_denied(response)

    def _request_resource(self, method: str, url: str,
                          **kwargs) -> Tuple[Union[None, Response], bool]:
        """
        Performs a simple GET request to the HTTP server with headers representing the given
        channel state.
        """
        headers = Munch()
        headers.contract_address = self.client.context.channel_manager.address
        if self.channel is not None:
            headers.balance = str(self.channel.balance)
            headers.balance_signature = encode_hex(self.channel.balance_sig)
            headers.sender_address = self.channel.sender
            headers.receiver_address = self.channel.receiver
            headers.open_block = str(self.channel.block)

        headers = HTTPHeaders.serialize(headers)
        if 'headers' in kwargs:
            headers.update(kwargs['headers'])
            kwargs['headers'] = headers
        else:
            kwargs['headers'] = headers
        response = requests.Session.request(self, method, url, **kwargs)

        if self.on_http_response(method, url, response, **kwargs) is False:
            return response, False  # user requested abort

        if response.status_code == requests.codes.OK:
            return response, self.on_success(method, url, response, **kwargs)

        elif response.status_code == requests.codes.PAYMENT_REQUIRED:
            if HTTPHeaders.NONEXISTING_CHANNEL in response.headers:
                return response, self.on_nonexisting_channel(
                    method, url, response, **kwargs)

            elif HTTPHeaders.INSUF_CONFS in response.headers:
                return response, self.on_insufficient_confirmations(
                    method, url, response, **kwargs)

            elif HTTPHeaders.INVALID_PROOF in response.headers:
                return response, self.on_invalid_balance_proof(
                    method, url, response, **kwargs)

            elif HTTPHeaders.CONTRACT_ADDRESS not in response.headers or not is_same_address(
                    response.headers.get(HTTPHeaders.CONTRACT_ADDRESS),
                    self.client.context.channel_manager.address):
                return response, self.on_invalid_contract_address(
                    method, url, response, **kwargs)

            elif HTTPHeaders.INVALID_AMOUNT in response.headers:
                return response, self.on_invalid_amount(
                    method, url, response, **kwargs)

            else:
                return response, self.on_payment_requested(
                    method, url, response, **kwargs)
        else:
            return response, self.on_http_error(method, url, response,
                                                **kwargs)

    def on_nonexisting_channel(self, method: str, url: str, response: Response,
                               **kwargs) -> bool:
        log.warning(
            'Channel not registered by server. Retrying in {} seconds.'.format(
                self.retry_interval))
        time.sleep(self.retry_interval)
        return True

    def on_insufficient_confirmations(self, method: str, url: str,
                                      response: Response, **kwargs) -> bool:
        log.warning(
            'Newly created channel does not have enough confirmations yet. Retrying in {} seconds.'
            .format(self.retry_interval))
        time.sleep(self.retry_interval)
        return True

    def on_invalid_balance_proof(self, method: str, url: str,
                                 response: Response, **kwargs) -> bool:
        log.warning(
            'Server was unable to verify the transfer - '
            'Either the balance was greater than deposit'
            'or the balance proof contained a lower balance than expected'
            'or possibly an unconfirmed or unregistered topup. Retrying in {} seconds.'
        )
        time.sleep(self.retry_interval)
        return True

    def on_invalid_amount(self, method: str, url: str, response: Response,
                          **kwargs) -> bool:
        log.debug('Server claims an invalid amount sent.')
        balance_sig = response.headers.get(HTTPHeaders.BALANCE_SIGNATURE)
        if balance_sig:
            balance_sig = decode_hex(balance_sig)
        last_balance = int(response.headers.get(HTTPHeaders.SENDER_BALANCE))

        verified = balance_sig and is_same_address(
            verify_balance_proof(self.channel.receiver, self.channel.block,
                                 last_balance, balance_sig,
                                 self.client.context.channel_manager.address),
            self.channel.sender)

        if verified:
            if last_balance == self.channel.balance:
                log.error(
                    'Server tried to disguise the last unconfirmed payment as a confirmed payment.'
                )
                return False
            else:
                log.debug(
                    'Server provided proof for a different channel balance ({}). Adopting.'
                    .format(last_balance))
                self.channel.update_balance(last_balance)
        else:
            log.debug(
                'Server did not provide proof for a different channel balance. Reverting to 0.'
            )
            self.channel.update_balance(0)

        return self.on_payment_requested(method, url, response, **kwargs)

    def on_payment_requested(self, method: str, url: str, response: Response,
                             **kwargs) -> bool:
        receiver = response.headers[HTTPHeaders.RECEIVER_ADDRESS]
        price = int(response.headers[HTTPHeaders.PRICE])
        assert price > 0

        log.debug('Preparing payment of price {} to {}.'.format(
            price, receiver))

        if self.channel is None or self.channel.state != Channel.State.open:
            new_channel = self.client.get_suitable_channel(
                receiver, price, self.initial_deposit, self.topup_deposit)

            if self.channel is not None and new_channel != self.channel:
                # This should only happen if there are multiple open channels to the target or a
                # channel has been closed while the session is still being used.
                log.warning(
                    'Channels switched. Previous balance proofs not applicable to new channel.'
                )

            self.channel = new_channel
        elif not self.channel.is_suitable(price):
            self.channel.topup(self.topup_deposit(price))

        if self.channel is None:
            log.error("No channel could be created or sufficiently topped up.")
            return False

        self.channel.create_transfer(price)
        log.debug(
            'Sending new balance proof. New channel balance: {}/{}'.format(
                self.channel.balance, self.channel.deposit))

        return True

    def on_http_error(self, method: str, url: str, response: Response,
                      **kwargs) -> bool:
        log.error('Unexpected server error, status code {}'.format(
            response.status_code))
        return False

    def on_init(self, method: str, url: str, **kwargs):
        log.debug('Starting {} request loop for resource at {}.'.format(
            method, url))

    def on_exit(self, method: str, url: str, response: Response, **kwargs):
        pass

    def on_success(self, method: str, url: str, response: Response,
                   **kwargs) -> bool:
        log.debug('Resource received.')
        cost = response.headers.get(HTTPHeaders.COST)
        if cost is not None:
            log.debug('Final cost was {}.'.format(cost))
        return False

    def on_invalid_contract_address(self, method: str, url: str,
                                    response: Response, **kwargs) -> bool:
        contract_address = response.headers.get(HTTPHeaders.CONTRACT_ADDRESS)
        log.error('Server sent no or invalid contract address: {}.'.format(
            contract_address))
        return False

    def on_cooperative_close_denied(self, response: Response = None):
        log.warning(
            'No valid closing signature received. Closing noncooperatively on a balance of 0.'
        )
        self.channel.close(0)

    def on_http_response(self, method: str, url: str, response: Response,
                         **kwargs) -> bool:
        """Called whenever server returns a reply.
        Return False to abort current request."""
        log.debug('Response received: {}'.format(response.headers))
        return True
Exemplo n.º 2
0
class Atn():
    def __init__(
            self,
            client: Client = None,
            retry_interval: int = 5,
            initial_deposit: Callable[[int], int] = lambda price: 10 * price,
            topup_deposit: Callable[[int], int] = lambda price: 5 * price,
            **client_kwargs
    ) -> None:
        self.client = client
        self.retry_interval = retry_interval
        self.initial_deposit = initial_deposit
        self.topup_deposit = topup_deposit

        self.channel = None  # type: Channel
        if self.client is None:
            self.client = Client(**client_kwargs)

    def get_dbot_name(self, dbot_address):
        Dbot = self.client.context.make_dbot_contract(dbot_address)
        name = Dbot.functions.name().call()
        return name.decode('utf-8').rstrip('\0')

    def get_dbot_domain(self, dbot_address):
        Dbot = self.client.context.make_dbot_contract(dbot_address)
        domain = Dbot.functions.domain().call()
        return domain.decode('utf-8').rstrip('\0')

    def get_price(self, dbot_address, uri, method):
        Dbot = self.client.context.make_dbot_contract(dbot_address)
        key = Dbot.functions.getKey(tobytes32(method.lower()), tobytes32(uri)).call()
        endpoint = Dbot.functions.keyToEndPoints(key).call()
        # TODO handle method case and how to check if the endpoint exist
        if (int(endpoint[1]) == 0):
            raise Exception('no such endpoint: uri = {}, method = {}'.format(uri, method))
        else:
            return int(endpoint[1])

    def get_channel_info(self, receiver):
        open_channels = self.client.get_open_channels(receiver)
        if open_channels:
            channel = open_channels[0]
            return channel
        else:
            return None

    def get_dbot_channel(self, dbot_address):
        domain = self.get_dbot_domain(dbot_address)
        channel = self.get_channel_info(dbot_address)
        backend = domain if domain.lower().startswith('http') else 'http://{}'.format(domain)
        if channel is not None:
            url = '{}/api/v1/dbots/{}/channels/{}/{}'.format(backend,
                                                             channel.receiver,
                                                             channel.sender,
                                                             channel.block
                                                             )
            print(url)
            resp = requests.get(url)
            if resp.status_code == 200:
                return resp.json()
            else:
                return None
        else:
            return None

    #  def retry_func(func, condition, retry_interval=5, retry_times=5)

    def test_call(self, dbot_address: str, uri: str, method: str, **kwargs) -> Response:
        price = self.get_price(dbot_address, uri, method)
        if self.channel is None or self.channel.state != Channel.State.open:
            self.channel = self.client.get_suitable_channel(
                dbot_address, price, self.initial_deposit, self.topup_deposit
            )
        if self.channel is None:
            log.error("No channel could be created or sufficiently topped up.")
            raise Exception('No channel could be created or sufficiently topped up.')

        dbot_channel = self.get_dbot_channel(dbot_address)

        retry_times = 5
        while retry_times > 0 and (dbot_channel is None or int(dbot_channel['deposit']) != self.channel.deposit):
            log.info('Channel has not synced by dbot server, retry after 5s')
            retry_times = retry_times - 1
            time.sleep(self.retry_interval)
            dbot_channel = self.get_dbot_channel(dbot_address)
        if dbot_channel is not None and int(dbot_channel['deposit']) == self.channel.deposit:
            self.channel.update_balance(int(dbot_channel['balance']))
        else:
            raise Exception('Channel can not synced by dbot server.')

        if not self.channel.is_suitable(price):
            self.channel.topup(self.topup_deposit(price))
            retry_times = 5
            while retry_times > 0 and (dbot_channel is None or int(dbot_channel['deposit']) != self.channel.deposit):
                log.info('Channel has not synced by dbot server, retry after 5s')
                retry_times = retry_times - 1
                time.sleep(self.retry_interval)
                dbot_channel = self.get_dbot_channel(dbot_address)
            if dbot_channel is not None and int(dbot_channel['deposit']) == self.channel.deposit:
                self.channel.update_balance(int(dbot_channel['balance']))
            else:
                raise Exception('Channel can not synced by dbot server.')

        self.channel.create_transfer(price)
        log.debug(
            'Sending new balance proof. New channel balance: {}/{}'
            .format(self.channel.balance, self.channel.deposit)
        )
        domain = self.get_dbot_domain(dbot_address)
        backend = domain if domain.lower().startswith('http') else 'http://{}'.format(domain)
        url = '{}/call/{}/{}'.format(backend, dbot_address, remove_slash_prefix(uri))
        response, retry = self._request_resource(method, url, **kwargs)
        if retry:
            raise Exception(response)
        return response

    def close_channel(self, endpoint_url: str = None):
        if self.channel is None:
            log.debug('No channel to close.')
            return

        if endpoint_url is None:
            endpoint_url = self.endpoint_url

        if endpoint_url is None:
            log.warning('No endpoint URL specified to request a closing signature.')
            self.on_cooperative_close_denied()
            return

        log.debug(
            'Requesting closing signature from server for balance {} on channel {}/{}/{}.'
            .format(
                self.channel.balance,
                self.channel.sender,
                self.channel.sender,
                self.channel.block
            )
        )
        url = '{}/api/1/channels/{}/{}'.format(
            endpoint_url,
            self.channel.sender,
            self.channel.block
        )

        # We need to call request directly because delete would perform a uRaiden request.
        try:
            response = requests.request(
                'DELETE',
                url,
                data={'balance': self.channel.balance}
            )
        except requests.exceptions.ConnectionError as err:
            log.error(
                'Could not get a response from the server while requesting a closing signature: {}'
                .format(err)
            )
            response = None

        failed = True
        if response is not None and response.status_code == requests.codes.OK:
            closing_sig = response.json()['close_signature']
            failed = self.channel.close_cooperatively(decode_hex(closing_sig)) is None

        if response is None or failed:
            self.on_cooperative_close_denied(response)

    def _request_resource(
            self,
            method: str,
            url: str,
            **kwargs
    ) -> Tuple[Union[None, Response], bool]:
        """
        Performs a simple GET request to the HTTP server with headers representing the given
        channel state.
        """
        headers = Munch()
        headers.contract_address = self.client.context.channel_manager.address
        if self.channel is not None:
            headers.balance = str(self.channel.balance)
            headers.balance_signature = encode_hex(self.channel.balance_sig)
            headers.sender_address = self.channel.sender
            headers.receiver_address = self.channel.receiver
            headers.open_block = str(self.channel.block)

        headers = HTTPHeaders.serialize(headers)
        if 'headers' in kwargs:
            headers.update(kwargs['headers'])
            kwargs['headers'] = headers
        else:
            kwargs['headers'] = headers
        response = requests.request(method, url, **kwargs)

        if self.on_http_response(method, url, response, **kwargs) is False:
            return response, False  # user requested abort

        if response.status_code == requests.codes.OK:
            return response, self.on_success(method, url, response, **kwargs)

        elif response.status_code == requests.codes.PAYMENT_REQUIRED:
            if HTTPHeaders.NONEXISTING_CHANNEL in response.headers:
                return response, self.on_nonexisting_channel(method, url, response, **kwargs)

            elif HTTPHeaders.INSUF_CONFS in response.headers:
                return response, self.on_insufficient_confirmations(
                    method,
                    url,
                    response,
                    **kwargs
                )

            elif HTTPHeaders.INVALID_PROOF in response.headers:
                return response, self.on_invalid_balance_proof(method, url, response, **kwargs)

            elif HTTPHeaders.CONTRACT_ADDRESS not in response.headers or not is_same_address(
                response.headers.get(HTTPHeaders.CONTRACT_ADDRESS),
                self.client.context.channel_manager.address
            ):
                return response, self.on_invalid_contract_address(method, url, response, **kwargs)

            elif HTTPHeaders.INVALID_AMOUNT in response.headers:
                return response, self.on_invalid_amount(method, url, response, **kwargs)

            else:
                return response, self.on_payment_requested(method, url, response, **kwargs)
        else:
            return response, self.on_http_error(method, url, response, **kwargs)

    def on_nonexisting_channel(
            self,
            method: str,
            url: str,
            response: Response,
            **kwargs
    ) -> bool:
        log.warning(
            'Channel not registered by server. Retrying in {} seconds.'
            .format(self.retry_interval)
        )
        time.sleep(self.retry_interval)
        return True

    def on_insufficient_confirmations(
            self,
            method: str,
            url: str,
            response: Response,
            **kwargs
    ) -> bool:
        log.warning(
            'Newly created channel does not have enough confirmations yet. Retrying in {} seconds.'
            .format(self.retry_interval)
        )
        time.sleep(self.retry_interval)
        return True

    def on_invalid_balance_proof(
        self,
        method: str,
        url: str,
        response: Response,
        **kwargs
    ) -> bool:
        log.warning(
            'Server was unable to verify the transfer - '
            'Either the balance was greater than deposit'
            'or the balance proof contained a lower balance than expected'
            'or possibly an unconfirmed or unregistered topup. Retrying in {} seconds.'
        )
        time.sleep(self.retry_interval)
        return True

    def on_invalid_amount(
            self,
            method: str,
            url: str,
            response: Response,
            **kwargs
    ) -> bool:
        log.debug('Server claims an invalid amount sent.')
        balance_sig = response.headers.get(HTTPHeaders.BALANCE_SIGNATURE)
        if balance_sig:
            balance_sig = decode_hex(balance_sig)
        last_balance = int(response.headers.get(HTTPHeaders.SENDER_BALANCE))

        verified = balance_sig and is_same_address(
            verify_balance_proof(
                self.channel.receiver,
                self.channel.block,
                last_balance,
                balance_sig,
                self.client.context.channel_manager.address
            ),
            self.channel.sender
        )

        if verified:
            if last_balance == self.channel.balance:
                log.error(
                    'Server tried to disguise the last unconfirmed payment as a confirmed payment.'
                )
                return False
            else:
                log.debug(
                    'Server provided proof for a different channel balance ({}). Adopting.'.format(
                        last_balance
                    )
                )
                self.channel.update_balance(last_balance)
        else:
            log.debug(
                'Server did not provide proof for a different channel balance. Reverting to 0.'
            )
            self.channel.update_balance(0)

        return self.on_payment_requested(method, url, response, **kwargs)

    def on_payment_requested(
            self,
            method: str,
            url: str,
            response: Response,
            **kwargs
    ) -> bool:
        receiver = response.headers[HTTPHeaders.RECEIVER_ADDRESS]
        if receiver and Web3.isAddress(receiver):
            receiver = Web3.toChecksumAddress(receiver)
        price = int(response.headers[HTTPHeaders.PRICE])
        assert price > 0

        log.debug('Preparing payment of price {} to {}.'.format(price, receiver))

        if self.channel is None or self.channel.state != Channel.State.open:
            new_channel = self.client.get_suitable_channel(
                receiver, price, self.initial_deposit, self.topup_deposit
            )

            if self.channel is not None and new_channel != self.channel:
                # This should only happen if there are multiple open channels to the target or a
                # channel has been closed while the session is still being used.
                log.warning(
                    'Channels switched. Previous balance proofs not applicable to new channel.'
                )

            self.channel = new_channel
        elif not self.channel.is_suitable(price):
            self.channel.topup(self.topup_deposit(price))

        if self.channel is None:
            log.error("No channel could be created or sufficiently topped up.")
            return False

        self.channel.create_transfer(price)
        log.debug(
            'Sending new balance proof. New channel balance: {}/{}'
            .format(self.channel.balance, self.channel.deposit)
        )

        return True

    def on_http_error(self, method: str, url: str, response: Response, **kwargs) -> bool:
        log.error(
            'Unexpected server error, status code {}'.format(response.status_code)
        )
        return False

    def on_init(self, method: str, url: str, **kwargs):
        log.debug('Starting {} request loop for resource at {}.'.format(method, url))

    def on_exit(self, method: str, url: str, response: Response, **kwargs):
        pass

    def on_success(self, method: str, url: str, response: Response, **kwargs) -> bool:
        log.debug('Resource received.')
        cost = response.headers.get(HTTPHeaders.COST)
        if cost is not None:
            log.debug('Final cost was {}.'.format(cost))
        return False

    def on_invalid_contract_address(
            self,
            method: str,
            url: str,
            response: Response,
            **kwargs
    ) -> bool:
        contract_address = response.headers.get(HTTPHeaders.CONTRACT_ADDRESS)
        log.error(
            'Server sent no or invalid contract address: {}.'.format(contract_address)
        )
        return False

    def on_cooperative_close_denied(self, response: Response = None):
        log.warning(
            'No valid closing signature received. Closing noncooperatively on a balance of 0.'
        )
        self.channel.close(0)

    def on_http_response(self, method: str, url: str, response: Response, **kwargs) -> bool:
        """Called whenever server returns a reply.
        Return False to abort current request."""
        log.debug('Response received: {}'.format(response.headers))
        return True
Exemplo n.º 3
0
class Session(requests.Session):
    def __init__(
            self,
            client: Client = None,
            endpoint_url: str = None,
            retry_interval: float = 5,
            initial_deposit: Callable[[int], int] = lambda price: 10 * price,
            topup_deposit: Callable[[int], int] = lambda price: 5 * price,
            close_channel_on_exit: bool = False,
            **client_kwargs
    ) -> None:
        requests.Session.__init__(self)
        self.channel = None  # type: Channel
        self.endpoint_url = endpoint_url
        self.client = client
        self.retry_interval = retry_interval
        self.initial_deposit = initial_deposit
        self.topup_deposit = topup_deposit
        self.close_channel_on_exit = close_channel_on_exit

        if self.client is None:
            self.client = Client(**client_kwargs)

    def close(self):
        if self.close_channel_on_exit and self.channel.state == Channel.State.open:
            self.close_channel()
        requests.Session.close(self)

    def request(self, method: str, url: str, **kwargs) -> Response:
        self.on_init(method, url, **kwargs)
        retry = True
        response = None
        while retry:
            response, retry = self._request_resource(method, url, **kwargs)

        self.on_exit(method, url, response, **kwargs)
        return response

    def close_channel(self, endpoint_url: str = None):
        if self.channel is None:
            log.debug('No channel to close.')
            return

        if endpoint_url is None:
            endpoint_url = self.endpoint_url

        if endpoint_url is None:
            log.warning('No endpoint URL specified to request a closing signature.')
            self.on_cooperative_close_denied()
            return

        log.debug(
            'Requesting closing signature from server for balance {} on channel {}/{}/{}.'
            .format(
                self.channel.balance,
                self.channel.sender,
                self.channel.sender,
                self.channel.block
            )
        )
        url = '{}/api/1/channels/{}/{}'.format(
            endpoint_url,
            self.channel.sender,
            self.channel.block
        )

        # We need to call request directly because delete would perform a uRaiden request.
        try:
            response = requests.Session.request(
                self,
                'DELETE',
                url,
                data={'balance': self.channel.balance}
            )
        except requests.exceptions.ConnectionError as err:
            log.error(
                'Could not get a response from the server while requesting a closing signature: {}'
                .format(err)
            )
            response = None

        failed = True
        if response is not None and response.status_code == requests.codes.OK:
            closing_sig = response.json()['close_signature']
            failed = self.channel.close_cooperatively(decode_hex(closing_sig)) is None

        if response is None or failed:
            self.on_cooperative_close_denied(response)

    def _request_resource(
            self,
            method: str,
            url: str,
            **kwargs
    ) -> Tuple[Union[None, Response], bool]:
        """
        Performs a simple GET request to the HTTP server with headers representing the given
        channel state.
        """
        headers = Munch()
        headers.contract_address = self.client.context.channel_manager.address
        if self.channel is not None:
            headers.balance = str(self.channel.balance)
            headers.balance_signature = encode_hex(self.channel.balance_sig)
            headers.sender_address = self.channel.sender
            headers.receiver_address = self.channel.receiver
            headers.open_block = str(self.channel.block)

        headers = HTTPHeaders.serialize(headers)
        if 'headers' in kwargs:
            headers.update(kwargs['headers'])
            kwargs['headers'] = headers
        else:
            kwargs['headers'] = headers
        response = requests.Session.request(self, method, url, **kwargs)

        if self.on_http_response(method, url, response, **kwargs) is False:
            return response, False  # user requested abort

        if response.status_code == requests.codes.OK:
            return response, self.on_success(method, url, response, **kwargs)

        elif response.status_code == requests.codes.PAYMENT_REQUIRED:
            if HTTPHeaders.NONEXISTING_CHANNEL in response.headers:
                return response, self.on_nonexisting_channel(method, url, response, **kwargs)

            elif HTTPHeaders.INSUF_CONFS in response.headers:
                return response, self.on_insufficient_confirmations(
                    method,
                    url,
                    response,
                    **kwargs
                )

            elif HTTPHeaders.INVALID_PROOF in response.headers:
                return response, self.on_invalid_balance_proof(method, url, response, **kwargs)

            elif HTTPHeaders.CONTRACT_ADDRESS not in response.headers or not is_same_address(
                response.headers.get(HTTPHeaders.CONTRACT_ADDRESS),
                self.client.context.channel_manager.address
            ):
                return response, self.on_invalid_contract_address(method, url, response, **kwargs)

            elif HTTPHeaders.INVALID_AMOUNT in response.headers:
                return response, self.on_invalid_amount(method, url, response, **kwargs)

            else:
                return response, self.on_payment_requested(method, url, response, **kwargs)
        else:
            return response, self.on_http_error(method, url, response, **kwargs)

    def on_nonexisting_channel(
            self,
            method: str,
            url: str,
            response: Response,
            **kwargs
    ) -> bool:
        log.warning(
            'Channel not registered by server. Retrying in {} seconds.'
            .format(self.retry_interval)
        )
        time.sleep(self.retry_interval)
        return True

    def on_insufficient_confirmations(
            self,
            method: str,
            url: str,
            response: Response,
            **kwargs
    ) -> bool:
        log.warning(
            'Newly created channel does not have enough confirmations yet. Retrying in {} seconds.'
            .format(self.retry_interval)
        )
        time.sleep(self.retry_interval)
        return True

    def on_invalid_balance_proof(
        self,
        method: str,
        url: str,
        response: Response,
        **kwargs
    ) -> bool:
        log.warning(
            'Server was unable to verify the transfer - '
            'Either the balance was greater than deposit'
            'or the balance proof contained a lower balance than expected'
            'or possibly an unconfirmed or unregistered topup. Retrying in {} seconds.'
        )
        time.sleep(self.retry_interval)
        return True

    def on_invalid_amount(
            self,
            method: str,
            url: str,
            response: Response,
            **kwargs
    ) -> bool:
        log.debug('Server claims an invalid amount sent.')
        balance_sig = response.headers.get(HTTPHeaders.BALANCE_SIGNATURE)
        if balance_sig:
            balance_sig = decode_hex(balance_sig)
        last_balance = int(response.headers.get(HTTPHeaders.SENDER_BALANCE))

        verified = balance_sig and is_same_address(
            verify_balance_proof(
                self.channel.receiver,
                self.channel.block,
                last_balance,
                balance_sig,
                self.client.context.channel_manager.address
            ),
            self.channel.sender
        )

        if verified:
            if last_balance == self.channel.balance:
                log.error(
                    'Server tried to disguise the last unconfirmed payment as a confirmed payment.'
                )
                return False
            else:
                log.debug(
                    'Server provided proof for a different channel balance ({}). Adopting.'.format(
                        last_balance
                    )
                )
                self.channel.update_balance(last_balance)
        else:
            log.debug(
                'Server did not provide proof for a different channel balance. Reverting to 0.'
            )
            self.channel.update_balance(0)

        return self.on_payment_requested(method, url, response, **kwargs)

    def on_payment_requested(
            self,
            method: str,
            url: str,
            response: Response,
            **kwargs
    ) -> bool:
        receiver = response.headers[HTTPHeaders.RECEIVER_ADDRESS]
        price = int(response.headers[HTTPHeaders.PRICE])
        assert price > 0

        log.debug('Preparing payment of price {} to {}.'.format(price, receiver))

        if self.channel is None or self.channel.state != Channel.State.open:
            new_channel = self.client.get_suitable_channel(
                receiver, price, self.initial_deposit, self.topup_deposit
            )

            if self.channel is not None and new_channel != self.channel:
                # This should only happen if there are multiple open channels to the target or a
                # channel has been closed while the session is still being used.
                log.warning(
                    'Channels switched. Previous balance proofs not applicable to new channel.'
                )

            self.channel = new_channel
        elif not self.channel.is_suitable(price):
            self.channel.topup(self.topup_deposit(price))

        if self.channel is None:
            log.error("No channel could be created or sufficiently topped up.")
            return False

        self.channel.create_transfer(price)
        log.debug(
            'Sending new balance proof. New channel balance: {}/{}'
            .format(self.channel.balance, self.channel.deposit)
        )

        return True

    def on_http_error(self, method: str, url: str, response: Response, **kwargs) -> bool:
        log.error(
            'Unexpected server error, status code {}'.format(response.status_code)
        )
        return False

    def on_init(self, method: str, url: str, **kwargs):
        log.debug('Starting {} request loop for resource at {}.'.format(method, url))

    def on_exit(self, method: str, url: str, response: Response, **kwargs):
        pass

    def on_success(self, method: str, url: str, response: Response, **kwargs) -> bool:
        log.debug('Resource received.')
        cost = response.headers.get(HTTPHeaders.COST)
        if cost is not None:
            log.debug('Final cost was {}.'.format(cost))
        return False

    def on_invalid_contract_address(
            self,
            method: str,
            url: str,
            response: Response,
            **kwargs
    ) -> bool:
        contract_address = response.headers.get(HTTPHeaders.CONTRACT_ADDRESS)
        log.error(
            'Server sent no or invalid contract address: {}.'.format(contract_address)
        )
        return False

    def on_cooperative_close_denied(self, response: Response = None):
        log.warning(
            'No valid closing signature received. Closing noncooperatively on a balance of 0.'
        )
        self.channel.close(0)

    def on_http_response(self, method: str, url: str, response: Response, **kwargs) -> bool:
        """Called whenever server returns a reply.
        Return False to abort current request."""
        log.debug('Response received: {}'.format(response.headers))
        return True