Beispiel #1
0
    def _private_api_query(
            self,
            endpoint: str,
            options: Optional[Dict[str, Any]] = None,
    ) -> Union[Dict[str, Any], List[Any]]:
        """Performs a Gemini API Query for a private endpoint

        You can optionally provide extra arguments to the endpoint via the options argument.

        Raises RemoteError if something went wrong with connecting or reading from the exchange
        Raises GeminiPermissionError if the API Key does not have sufficient
        permissions for the endpoint
        """
        response = self._query_continuously(method='post', endpoint=endpoint, options=options)
        json_ret: Union[List[Any], Dict[str, Any]]
        if response.status_code == HTTPStatus.FORBIDDEN:
            raise GeminiPermissionError(
                f'API key does not have permission for {endpoint}',
            )
        if response.status_code == HTTPStatus.BAD_REQUEST:
            if 'InvalidSignature' in response.text:
                raise GeminiPermissionError('Invalid API Key or API secret')
            # else let it be handled by the generic non-200 code error below

        if response.status_code != HTTPStatus.OK:
            raise RemoteError(
                f'Gemini query at {response.url} responded with error '
                f'status code: {response.status_code} and text: {response.text}',
            )

        deserialization_fn: Union[Callable[[str], Dict[str, Any]], Callable[[str], List[Any]]]
        deserialization_fn = jsonloads_dict if endpoint == 'roles' else jsonloads_list

        try:
            json_ret = deserialization_fn(response.text)
        except JSONDecodeError as e:
            raise RemoteError(
                f'Gemini query at {response.url} '
                f'returned invalid JSON response: {response.text}',
            ) from e

        return json_ret
Beispiel #2
0
def _check_and_get_response(response: Response, method: str) -> Union[str, Dict]:
    """Checks the kraken response and if it's succesfull returns the result.

    If there is recoverable error a string is returned explaining the error
    May raise:
    - RemoteError if there is an unrecoverable/unexpected remote error
    """
    if response.status_code in (520, 525, 504):
        log.debug(f'Kraken returned status code {response.status_code}')
        return 'Usual kraken 5xx shenanigans'
    elif response.status_code != 200:
        raise RemoteError(
            'Kraken API request {} for {} failed with HTTP status '
            'code: {}'.format(
                response.url,
                method,
                response.status_code,
            ))

    try:
        decoded_json = rlk_jsonloads_dict(response.text)
    except json.decoder.JSONDecodeError as e:
        raise RemoteError(f'Invalid JSON in Kraken response. {e}')

    try:
        if decoded_json['error']:
            if isinstance(decoded_json['error'], list):
                error = decoded_json['error'][0]
            else:
                error = decoded_json['error']

            if 'Rate limit exceeded' in error:
                log.debug(f'Kraken: Got rate limit exceeded error: {error}')
                return 'Rate limited exceeded'
            else:
                raise RemoteError(error)

        result = decoded_json['result']
    except KeyError as e:
        raise RemoteError(f'Unexpected format of Kraken response. Missing key: {e}')

    return result
Beispiel #3
0
def _parse_int(line: str, entry: str) -> int:
    try:
        if line == '-':
            result = 0
        else:
            result = int(line)
    except ValueError as e:
        raise RemoteError(
            f'Could not parse {line} as an integer for {entry}') from e

    return result
Beispiel #4
0
 def __init__(self, url: str) -> None:
     """
     - May raise requests.RequestException if there is a problem connecting to the subgraph"""
     transport = RequestsHTTPTransport(url=url)
     try:
         self.client = Client(transport=transport,
                              fetch_schema_from_transport=False)
     except (requests.exceptions.RequestException) as e:
         raise RemoteError(
             f'Failed to connect to the graph at {url} due to {str(e)}'
         ) from e
Beispiel #5
0
def request_get(uri, timeout=ALL_REMOTES_TIMEOUT):
    response = requests.get(uri)
    if response.status_code != 200:
        raise RemoteError('Get {} returned status code {}'.format(uri, response.status_code))

    try:
        result = rlk_jsonloads(response.text)
    except json.decoder.JSONDecodeError:
        raise ValueError('{} returned malformed json'.format(uri))

    return result
Beispiel #6
0
    def api_query(
            self,
            method: str,
            options: Optional[Dict] = None,
    ) -> Union[List, Dict]:
        """
        Queries Bittrex with given method and options
        """
        if not options:
            options = {}
        nonce = str(int(time.time() * 1000))
        method_type = 'public'

        if method in BITTREX_MARKET_METHODS:
            method_type = 'market'
        elif method in BITTREX_ACCOUNT_METHODS:
            method_type = 'account'

        request_url = self.uri + method_type + '/' + method + '?'

        if method_type != 'public':
            request_url += 'apikey=' + self.api_key.decode() + "&nonce=" + nonce + '&'

        request_url += urlencode(options)
        signature = hmac.new(
            self.secret,
            request_url.encode(),
            hashlib.sha512,
        ).hexdigest()
        self.session.headers.update({'apisign': signature})
        log.debug('Bittrex API query', request_url=request_url)
        response = self.session.get(request_url)

        try:
            json_ret = rlk_jsonloads_dict(response.text)
        except JSONDecodeError:
            raise RemoteError('Bittrex returned invalid JSON response')

        if json_ret['success'] is not True:
            raise RemoteError(json_ret['message'])
        return json_ret['result']
Beispiel #7
0
    def _api_query(self, command: str, req: Optional[Dict] = None) -> Union[Dict, List]:
        if req is None:
            req = {}

        if command == 'returnTicker' or command == 'returnCurrencies':
            log.debug(f'Querying poloniex for {command}')
            ret = self.session.get(self.public_uri + command)
            return rlk_jsonloads(ret.text)

        req['command'] = command
        with self.lock:
            # Protect this region with a lock since poloniex will reject
            # non-increasing nonces. So if two greenlets come in here at
            # the same time one of them will fail
            req['nonce'] = int(time.time() * 1000)
            post_data = str.encode(urlencode(req))

            sign = hmac.new(self.secret, post_data, hashlib.sha512).hexdigest()
            self.session.headers.update({'Sign': sign})

            log.debug(
                'Poloniex private API query',
                command=command,
                post_data=req,
            )
            ret = self.session.post('https://poloniex.com/tradingApi', req)

        if ret.status_code != 200:
            raise RemoteError(
                f'Poloniex query responded with error status code: {ret.status_code}'
                f' and text: {ret.text}',
            )

        try:
            if command == 'returnLendingHistory':
                return rlk_jsonloads_list(ret.text)
            else:
                result = rlk_jsonloads_dict(ret.text)
                return _post_process(result)
        except JSONDecodeError:
            raise RemoteError(f'Poloniex returned invalid JSON response: {ret.text}')
Beispiel #8
0
    def _get_account_balance(
            self,
            account: SubstrateAddress,
            node_interface: SubstrateInterface,
    ) -> FVal:
        """Given an account get its amount of chain native token.

        More information about an account balance in the Substrate AccountData
        documentation.
        """
        log.debug(
            f'{self.chain} querying {self.chain_properties.token.identifier} balance',
            url=node_interface.url,
            account=account,
        )
        try:
            with gevent.Timeout(KUSAMA_NODE_CONNECTION_TIMEOUT):
                result = node_interface.query(
                    module='System',
                    storage_function='Account',
                    params=[account],
                )
        except (
                requests.exceptions.RequestException,
                SubstrateRequestException,
                ValueError,
                WebSocketException,
                gevent.Timeout,
        ) as e:
            msg = str(e)
            if isinstance(e, gevent.Timeout):
                msg = f'a timeout of {msg}'
            message = (
                f'{self.chain} failed to request {self.chain_properties.token.identifier} account '
                f'balance at endpoint {node_interface.url} due to: {msg}'
            )
            log.error(message, account=account)
            raise RemoteError(message) from e

        log.debug(
            f'{self.chain} account balance',
            account=account,
            result=result,
        )
        balance = ZERO
        if result is not None:
            account_data = result.value['data']
            balance = (
                FVal(account_data['free'] + account_data['reserved']) /
                FVal('10') ** self.chain_properties.token_decimals
            )

        return balance
Beispiel #9
0
    def _api_query(
        self,
        endpoint: Literal['balance', 'user_transactions'],
        method: Literal['post'] = 'post',
        options: Optional[Dict[str, Any]] = None,
    ) -> Response:
        """Request a Bistamp API v2 endpoint (from `endpoint`).
        """
        call_options = options.copy() if options else {}
        data = call_options or None
        request_url = f'{self.base_uri}/v2/{endpoint}/'
        query_params = ''
        nonce = str(uuid.uuid4())
        timestamp = str(ts_now_in_ms())
        payload_string = urlencode(call_options)
        content_type = '' if payload_string == '' else 'application/x-www-form-urlencoded'
        message = ('BITSTAMP '
                   f'{self.api_key}'
                   f'{method.upper()}'
                   f'{request_url.replace("https://", "")}'
                   f'{query_params}'
                   f'{content_type}'
                   f'{nonce}'
                   f'{timestamp}'
                   'v2'
                   f'{payload_string}')
        signature = hmac.new(
            self.secret,
            msg=message.encode('utf-8'),
            digestmod=hashlib.sha256,
        ).hexdigest()

        self.session.headers.update({
            'X-Auth-Signature': signature,
            'X-Auth-Nonce': nonce,
            'X-Auth-Timestamp': timestamp,
        })
        if content_type:
            self.session.headers.update({'Content-Type': content_type})

        log.debug('Bitstamp API request', request_url=request_url)
        try:
            response = self.session.request(
                method=method,
                url=request_url,
                data=data,
            )
        except requests.exceptions.RequestException as e:
            raise RemoteError(
                f'Bitstamp {method} request at {request_url} connection error: {str(e)}.',
            ) from e

        return response
Beispiel #10
0
    def query_balances(self, **kwargs: Any) -> ExchangeQueryBalances:
        assets_balance: Dict[Asset, Balance] = {}
        try:
            resp_info = self._api_query('get', 'user/balance')
        except RemoteError as e:
            msg = (
                'ICONOMI API request failed. Could not reach ICONOMI due '
                'to {}'.format(e)
            )
            log.error(msg)
            return None, msg

        if resp_info['currency'] != 'USD':
            raise RemoteError('Iconomi API did not return values in USD')

        for balance_info in resp_info['assetList']:
            ticker = balance_info['ticker']
            try:
                asset = iconomi_asset(ticker)

                # There seems to be a bug in the ICONOMI API regarding balance_info['value'].
                # The value is supposed to be in USD, but is actually returned
                # in EUR. So let's use the Inquirer for now.
                try:
                    usd_price = Inquirer().find_usd_price(asset=asset)
                except RemoteError as e:
                    self.msg_aggregator.add_error(
                        f'Error processing ICONOMI balance entry due to inability to '
                        f'query USD price: {str(e)}. Skipping balance entry',
                    )
                    continue

                amount = FVal(balance_info['balance'])
                assets_balance[asset] = Balance(
                    amount=amount,
                    usd_value=amount * usd_price,
                )
            except (UnknownAsset, UnsupportedAsset) as e:
                asset_tag = 'unknown' if isinstance(e, UnknownAsset) else 'unsupported'
                self.msg_aggregator.add_warning(
                    f'Found {asset_tag} ICONOMI asset {ticker}. '
                    f' Ignoring its balance query.',
                )

        for balance_info in resp_info['daaList']:
            ticker = balance_info['ticker']
            self.msg_aggregator.add_warning(
                f'Found unsupported ICONOMI strategy {ticker}. '
                f' Ignoring its balance query.',
            )

        return assets_balance, ''
Beispiel #11
0
    def query(
        self,
        querystr: str,
        param_types: Optional[Dict[str, Any]] = None,
        param_values: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Queries The Graph for a particular query

        May raise:
        - RemoteError: If there is a problem querying the subgraph and there
        are no retries left.
        """
        prefix = ''
        if param_types is not None:
            prefix = 'query '
            prefix += json.dumps(param_types).replace('"', '').replace(
                '{', '(').replace('}', ')')
            prefix += '{'

        querystr = prefix + querystr
        log.debug(f'Querying The Graph for {querystr}')

        retries_left = QUERY_RETRY_TIMES
        while retries_left > 0:
            try:
                result = self.client.execute(gql(querystr),
                                             variable_values=param_values)
            # need to catch Exception here due to stupidity of gql library
            except (requests.exceptions.RequestException, Exception) as e:  # pylint: disable=broad-except  # noqa: E501
                # NB: the lack of a good API error handling by The Graph combined
                # with gql v2 raising bare exceptions doesn't allow us to act
                # better on failed requests. Currently all trigger the retry logic.
                # TODO: upgrade to gql v3 and amend this code on any improvement
                # The Graph does on its API error handling.
                exc_msg = str(e)
                retries_left -= 1
                base_msg = f'The Graph query to {querystr} failed due to {exc_msg}'
                if retries_left:
                    sleep_seconds = RETRY_BACKOFF_FACTOR * pow(
                        2, QUERY_RETRY_TIMES - retries_left)
                    retry_msg = (
                        f'Retrying query after {sleep_seconds} seconds. '
                        f'Retries left: {retries_left}.')
                    log.error(f'{base_msg}. {retry_msg}')
                    gevent.sleep(sleep_seconds)
                else:
                    raise RemoteError(f'{base_msg}. No retries left.') from e
            else:
                break

        log.debug('Got result from The Graph query')
        return result
Beispiel #12
0
def test_query_balances_skips_inquirer_error(mock_bitstamp):
    """Test an entry that can't get its USD price because of a remote error is
    skipped
    """
    inquirer = MagicMock()
    inquirer.find_usd_price.side_effect = RemoteError('test')

    def mock_api_query_response(endpoint):  # pylint: disable=unused-argument
        return MockResponse(HTTPStatus.OK, '{"link_balance": "1.00000000"}')

    with patch('rotkehlchen.exchanges.bitstamp.Inquirer', return_value=inquirer):
        with patch.object(mock_bitstamp, '_api_query', side_effect=mock_api_query_response):
            assert mock_bitstamp.query_balances() == ({}, '')
Beispiel #13
0
def get_poap_airdrop_data(name: str, data_dir: Path) -> Dict[str, Any]:
    airdrops_dir = data_dir / 'airdrops_poap'
    airdrops_dir.mkdir(parents=True, exist_ok=True)
    filename = airdrops_dir / f'{name}.json'
    if not filename.is_file():
        # if not cached, get it from the gist
        try:
            request = requests.get(POAP_AIRDROPS[name][0])
        except requests.exceptions.RequestException as e:
            raise RemoteError(f'POAP airdrops Gist request failed due to {str(e)}') from e

        try:
            json_data = rlk_jsonloads_dict(request.content.decode('utf-8'))
        except JSONDecodeError as e:
            raise RemoteError(f'POAP airdrops Gist contains an invalid JSON {str(e)}') from e

        with open(filename, 'w') as outfile:
            outfile.write(rlk_jsondumps(json_data))

    infile = open(filename, 'r')
    data_dict = rlk_jsonloads_dict(infile.read())
    return data_dict
Beispiel #14
0
    def _check_for_system_clock_not_synced_error(response: Response) -> None:
        if response.status_code == HTTPStatus.UNAUTHORIZED:
            try:
                result = rlk_jsonloads_dict(response.text)
            except JSONDecodeError:
                raise RemoteError(f'Bittrex returned invalid JSON response: {response.text}')

            if result.get('code', None) == 'INVALID_TIMESTAMP':
                raise SystemClockNotSyncedError(
                    current_time=str(datetime.now()),
                    remote_server='Bittrex',
                )
        return None
Beispiel #15
0
    def _query(self, path: str) -> Dict:
        """
        May raise:
        - RemoteError if there is a problem querying Github
        """
        try:
            response = requests.get(f'{self.prefix}{path}')
        except requests.exceptions.RequestException as e:
            raise RemoteError(f'Failed to query Github: {str(e)}')

        if response.status_code != 200:
            raise RemoteError(
                f'Github API request {response.url} for {path} failed '
                f'with HTTP status code {response.status_code} and text '
                f'{response.text}',
            )

        try:
            json_ret = rlk_jsonloads_dict(response.text)
        except JSONDecodeError:
            raise RemoteError(f'Github returned invalid JSON response: {response.text}')
        return json_ret
Beispiel #16
0
    def _query_continuously(
        self,
        method: Literal['get', 'post'],
        endpoint: str,
        options: Optional[Dict[str, Any]] = None,
    ) -> requests.Response:
        """Queries endpoint until anything but 429 is returned

        May raise:
        - RemoteError if something is wrong connecting to the exchange
        """
        v_endpoint = f'/v1/{endpoint}'
        url = f'{self.base_uri}{v_endpoint}'
        retries_left = QUERY_RETRY_TIMES
        while retries_left > 0:
            if endpoint in ('mytrades', 'balances', 'transfers', 'roles'):
                # private endpoints
                timestamp = str(ts_now_in_ms())
                payload = {'request': v_endpoint, 'nonce': timestamp}
                if options is not None:
                    payload.update(options)
                encoded_payload = json.dumps(payload).encode()
                b64 = b64encode(encoded_payload)
                signature = hmac.new(self.secret, b64,
                                     hashlib.sha384).hexdigest()

                self.session.headers.update({
                    'X-GEMINI-PAYLOAD': b64.decode(),
                    'X-GEMINI-SIGNATURE': signature,
                })

            try:
                response = self.session.request(
                    method=method,
                    url=url,
                    timeout=GLOBAL_REQUESTS_TIMEOUT,
                )
            except requests.exceptions.RequestException as e:
                raise RemoteError(
                    f'Gemini {method} query at {url} connection error: {str(e)}',
                ) from e

            if response.status_code == HTTPStatus.TOO_MANY_REQUESTS:
                # Backoff a bit by sleeping. Sleep more, the more retries have been made
                gevent.sleep(QUERY_RETRY_TIMES / retries_left)
                retries_left -= 1
            else:
                # get out of the retry loop, we did not get 429 complaint
                break

        return response
Beispiel #17
0
    def _query_lending_balances(
        self,
        balances: DefaultDict[Asset, Balance],
    ) -> DefaultDict[Asset, Balance]:
        data = self.api_query_dict('sapi', 'lending/union/account')
        positions = data.get('positionAmountVos', None)
        if positions is None:
            raise RemoteError(
                f'Could not find key positionAmountVos in lending account data '
                f'{data} returned by {self.name}.', )

        for entry in positions:
            try:
                amount = FVal(entry['amount'])
                if amount == ZERO:
                    continue

                asset = asset_from_binance(entry['asset'])
            except UnsupportedAsset as e:
                self.msg_aggregator.add_warning(
                    f'Found unsupported {self.name} asset {e.asset_name}. '
                    f'Ignoring its lending balance query.', )
                continue
            except UnknownAsset as e:
                self.msg_aggregator.add_warning(
                    f'Found unknown {self.name} asset {e.asset_name}. '
                    f'Ignoring its lending balance query.', )
                continue
            except (DeserializationError, KeyError) as e:
                msg = str(e)
                if isinstance(e, KeyError):
                    msg = f'Missing key entry for {msg}.'
                self.msg_aggregator.add_error(
                    f'Error at deserializing {self.name} asset. {msg}. '
                    f'Ignoring its lending balance query.', )
                continue

            try:
                usd_price = Inquirer().find_usd_price(asset)
            except RemoteError as e:
                self.msg_aggregator.add_error(
                    f'Error processing {self.name} balance entry due to inability to '
                    f'query USD price: {str(e)}. Skipping balance entry', )
                continue

            balances[asset] += Balance(
                amount=amount,
                usd_value=amount * usd_price,
            )

        return balances
Beispiel #18
0
    def get_balance_history(
            self, validator_indices: List[int]) -> Dict[int, ValidatorBalance]:
        """Get the balance history of all the validators given from the indices list

        https://beaconcha.in/api/v1/docs/index.html#/Validator/get_api_v1_validator__indexOrPubkey__balancehistory

        Queries in chunks of 100 due to api limitations.

        NOTICE: Do not use yet. The results seem incosistent. The list can accept
        up to 100 validators, but the balance history is for the last 100 epochs
        of each validator, limited to 100 results. So it's not really useful.

        Their devs said they will have a look as this may not be desired behaviour.

        May raise:
        - RemoteError due to problems querying beaconcha.in API
        """
        chunks = list(get_chunks(validator_indices, n=100))
        data = []
        for chunk in chunks:
            result = self._query(
                module='validator',
                endpoint='balancehistory',
                encoded_args=','.join(str(x) for x in chunk),
            )
            if isinstance(result, list):
                data.extend(result)
            else:
                data.append(result)

        # We are only interested in last epoch, so get its value
        balances: Dict[int, ValidatorBalance] = {}
        try:
            for entry in data:
                index = entry['validatorindex']
                epoch = entry['epoch']
                if index in balances and balances[index].epoch >= epoch:
                    continue

                balances[index] = ValidatorBalance(
                    epoch=epoch,
                    balance=entry['balance'],
                    effective_balance=entry['effectivebalance'],
                )
        except KeyError as e:
            raise RemoteError(
                f'Beaconchai.in balance response processing error. Missing key entry {str(e)}',
            ) from e

        return balances
Beispiel #19
0
    def _request_chain_metadata(self) -> Dict[str, Any]:
        """Subscan API metadata documentation:
        https://docs.api.subscan.io/#metadata
        """
        response = self._request_explorer_api(endpoint='metadata')
        if response.status_code != HTTPStatus.OK:
            message = (
                f'{self.chain} chain metadata request was not successful. '
                f'Response status code: {response.status_code}. '
                f'Response text: {response.text}.')
            log.error(message)
            raise RemoteError(message)
        try:
            result = rlk_jsonloads_dict(response.text)
        except JSONDecodeError as e:
            message = (
                f'{self.chain} chain metadata request returned invalid JSON '
                f'response: {response.text}.')
            log.error(message)
            raise RemoteError(message) from e

        log.debug(f'{self.chain} subscan API metadata', result=result)
        return result
Beispiel #20
0
    def get_account_balance(self, account: ChecksumEthAddress) -> FVal:
        """Gets the balance of the given account in WEI

        May raise:
        - RemoteError due to self._query(). Also if the returned result can't be parsed as a number
        """
        result = self._query(module='account', action='balance', options={'address': account})
        try:
            amount = FVal(result)
        except ValueError:
            raise RemoteError(
                f'Etherscan returned non-numeric result for account balance {result}',
            )
        return amount
Beispiel #21
0
def _decode_response_json(response: requests.Response) -> Any:
    """Decodes a python requests response to json and returns it.

    May raise:
    - RemoteError if the response does not contain valid json
    """
    try:
        json_response = response.json()
    except ValueError as e:
        raise RemoteError(
            f'Could not decode json from {response.text} to {response.request.method} '
            f'query {response.url}', ) from e

    return json_response
Beispiel #22
0
    def api_query(self, method: str, req: Optional[dict] = None) -> dict:
        tries = KRAKEN_QUERY_TRIES
        query_method = (self._query_public if method in KRAKEN_PUBLIC_METHODS
                        else self._query_private)
        while tries > 0:
            if self.call_counter + MAX_CALL_COUNTER_INCREASE > self.call_limit:
                # If we are close to the limit, check how much our call counter reduced
                # https://www.kraken.com/features/api#api-call-rate-limit
                secs_since_last_call = ts_now() - self.last_query_ts
                self.call_counter = max(
                    0,
                    self.call_counter -
                    int(secs_since_last_call / self.reduction_every_secs),
                )
                # If still at limit, sleep for an amount big enough for smallest tier reduction
                if self.call_counter + MAX_CALL_COUNTER_INCREASE > self.call_limit:
                    backoff_in_seconds = self.reduction_every_secs * 2
                    log.debug(
                        f'Doing a Kraken API call would now exceed our call counter limit. '
                        f'Backing off for {backoff_in_seconds} seconds',
                        call_counter=self.call_counter,
                    )
                    tries -= 1
                    gevent.sleep(backoff_in_seconds)
                    continue

            log.debug(
                'Kraken API query',
                method=method,
                data=req,
                call_counter=self.call_counter,
            )
            result = query_method(method, req)
            if isinstance(result, str):
                # Got a recoverable error
                backoff_in_seconds = int(KRAKEN_BACKOFF_DIVIDEND / tries)
                log.debug(
                    f'Got recoverable error {result} in a Kraken query of {method}. Will backoff '
                    f'for {backoff_in_seconds} seconds', )
                tries -= 1
                gevent.sleep(backoff_in_seconds)
                continue

            # else success
            return result

        raise RemoteError(
            f'After {KRAKEN_QUERY_TRIES} kraken queries for {method} could still not be completed',
        )
Beispiel #23
0
    def _request_explorer_api(self, endpoint: Literal['metadata']) -> Response:
        if endpoint == 'metadata':
            url = f'{self.chain.chain_explorer_api()}/scan/metadata'
        else:
            raise AssertionError(f'Unexpected {self.chain} endpoint type: {endpoint}')

        log.debug(f'{self.chain} subscan API request', request_url=url)
        try:
            response = requests.post(url=url)
        except requests.exceptions.RequestException as e:
            message = f'{self.chain} failed to post request at {url}. Connection error: {str(e)}.'
            log.error(message)
            raise RemoteError(message) from e

        return response
Beispiel #24
0
    def _query(self, path: str) -> str:
        backoff = INITIAL_BACKOFF
        while True:
            response = self.session.get(f'{self.prefix}{path}')
            if response.status_code == 429 and backoff < self.backoff_limit:
                gevent.sleep(backoff)
                backoff *= 2
                continue
            elif response.status_code != 200:
                raise RemoteError(
                    f'Coinpaprika API request {response.url} for {path} failed '
                    f'with HTTP status code {response.status_code} and text '
                    f'{response.text}', )

            return response.text
Beispiel #25
0
    def _query(
            self,
            module: Literal['validator'],
            endpoint: Literal['balancehistory', 'performance', 'eth1'],
            encoded_args: str,
    ) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
        """
        May raise:
        - RemoteError due to problems querying beaconcha.in API
        """
        if endpoint == 'eth1':
            query_str = f'{self.url}{module}/{endpoint}/{encoded_args}'
        else:
            query_str = f'{self.url}{module}/{encoded_args}/{endpoint}'
        times = QUERY_RETRY_TIMES
        backoff_in_seconds = 10

        while True:
            try:
                response = self.session.get(query_str)
            except requests.exceptions.RequestException as e:
                raise RemoteError(f'Querying {query_str} failed due to {str(e)}')

            if response.status_code == 429:
                if times == 0:
                    raise RemoteError(
                        f'Beaconchain API request {response.url} failed '
                        f'with HTTP status code {response.status_code} and text '
                        f'{response.text} after 5 retries',
                    )

                # We got rate limited. Let's try incremental backoff
                gevent.sleep(backoff_in_seconds * (QUERY_RETRY_TIMES - times + 1))
                continue
            else:
                break

        if response.status_code != 200:
            raise RemoteError(
                f'Beaconchain API request {response.url} failed '
                f'with HTTP status code {response.status_code} and text '
                f'{response.text}',
            )

        try:
            json_ret = rlk_jsonloads_dict(response.text)
        except JSONDecodeError:
            raise RemoteError(f'Beaconchain API returned invalid JSON response: {response.text}')

        if json_ret.get('status') != 'OK':
            raise RemoteError(f'Beaconchain API returned non-OK status. Response: {json_ret}')

        if 'data' not in json_ret:
            raise RemoteError(f'Beaconchain API did not contain a data key. Response: {json_ret}')

        return json_ret['data']
Beispiel #26
0
    def _check_node_synchronization(self, node_interface: SubstrateInterface) -> BlockNumber:
        """Check the node synchronization comparing the last block obtained via
        the node interface against the last block obtained via Subscan API.
        Return the last block obtained via the node interface.

        May raise:
        - RemoteError: the last block/chain metadata requests fail or
        there is an error deserializing the chain metadata.
        """
        # Last block via node interface
        last_block = self._get_last_block(node_interface=node_interface)

        # Last block via Subscan API
        try:
            chain_metadata = self._request_chain_metadata()
        except RemoteError:
            self.msg_aggregator.add_warning(
                f'Unable to verify that {self.chain} node at endpoint {node_interface.url} '
                f'is synced with the chain. Balances and other queries may be incorrect.',
            )
            return last_block

        # Check node synchronization
        try:
            metadata_last_block = BlockNumber(
                deserialize_int_from_str(
                    symbol=chain_metadata['data']['blockNum'],
                    location='subscan api',
                ),
            )
        except (KeyError, DeserializationError) as e:
            message = f'{self.chain} failed to deserialize the chain metadata response: {str(e)}.'
            log.error(message, chain_metadata=chain_metadata)
            raise RemoteError(message) from e

        log.debug(
            f'{self.chain} subscan API metadata last block',
            metadata_last_block=metadata_last_block,
        )
        if metadata_last_block - last_block > self.chain.blocks_threshold():
            self.msg_aggregator.add_warning(
                f'Found that {self.chain} node at endpoint {node_interface.url} '
                f'is not synced with the chain. Node last block is {last_block}, '
                f'expected last block is {metadata_last_block}. '
                f'Balances and other queries may be incorrect.',
            )

        return last_block
Beispiel #27
0
    def _get_chain_id(self, node_interface: SubstrateInterface) -> SubstrateChainId:
        """Return the chain identifier.
        """
        log.debug(f'{self.chain} querying chain ID', url=node_interface.url)
        try:
            chain_id = node_interface.chain
        except (requests.exceptions.RequestException, SubstrateRequestException) as e:
            message = (
                f'{self.chain} failed to request chain ID '
                f'at endpoint: {node_interface.url} due to: {str(e)}.'
            )
            log.error(message)
            raise RemoteError(message) from e

        log.debug(f'{self.chain} chain ID', chain_id=chain_id)
        return SubstrateChainId(chain_id)
Beispiel #28
0
    def _check_chain_id(self, node_interface: SubstrateInterface) -> None:
        """Validate a node connects to the expected chain.

        May raise:
        - RemoteError: the chain ID request fails, or the chain ID is not the
        expected one.
        """
        # Check connection and chain ID
        chain_id = self._get_chain_id(node_interface=node_interface)

        if chain_id != str(self.chain):
            message = (
                f'{self.chain} found unexpected chain {chain_id} when attempted '
                f'to connect to node at endpoint: {node_interface.url}, ')
            log.error(message)
            raise RemoteError(message)
Beispiel #29
0
def wait_until_a_node_is_available(
    substrate_manager: SubstrateManager,
    seconds: int,
) -> None:
    """Temporarily suspends the caller execution until a node is available or
    this function timeouts.
    """
    try:
        with gevent.Timeout(seconds):
            while len(substrate_manager.available_nodes_call_order) == 0:
                gevent.sleep(0.1)
    except gevent.Timeout as e:
        chain = substrate_manager.chain
        raise RemoteError(
            f"{chain} manager does not have nodes availables after waiting "
            f"{seconds} seconds. {chain} balances won't be queried.", ) from e
Beispiel #30
0
    def api_query(self,
                  method: str,
                  options: Optional[Dict] = None) -> Union[List, Dict]:
        if not options:
            options = {}

        with self.lock:
            # Protect this region with a lock since binance will reject
            # non-increasing nonces. So if two greenlets come in here at
            # the same time one of them will fail
            if method in V3_ENDPOINTS:
                api_version = 3
                # Recommended recvWindows is 5000 but we get timeouts with it
                options['recvWindow'] = 10000
                options['timestamp'] = str(int(time.time() * 1000))
                signature = hmac.new(self.secret,
                                     urlencode(options).encode('utf-8'),
                                     hashlib.sha256).hexdigest()
                options['signature'] = signature
            elif method in V1_ENDPOINTS:
                api_version = 1
            else:
                raise ValueError(
                    'Unexpected binance api method {}'.format(method))

            request_url = self.uri + 'v' + str(
                api_version) + '/' + method + '?'
            request_url += urlencode(options)

            log.debug('Binance API request', request_url=request_url)

            response = self.session.get(request_url)

        if response.status_code != 200:
            result = rlk_jsonloads(response.text)
            raise RemoteError(
                'Binance API request {} for {} failed with HTTP status '
                'code: {}, error code: {} and error message: {}'.format(
                    response.url,
                    method,
                    response.status_code,
                    result['code'],
                    result['msg'],
                ))

        json_ret = rlk_jsonloads(response.text)
        return json_ret