Esempio n. 1
0
class PolygonCovalentApi(CovalentApiBase):

    CHAIN_ID = 137

    api_options = ApiOptions(
        blockchain=Blockchain.POLYGON,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_MATIC
Esempio n. 2
0
class FantomCovalentApi(CovalentApiBase):

    CHAIN_ID = 250

    api_options = ApiOptions(
        blockchain=Blockchain.FANTOM,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_FTM
Esempio n. 3
0
class BscCovalentApi(CovalentApiBase):

    CHAIN_ID = 56

    api_options = ApiOptions(
        blockchain=Blockchain.BINANCE_SMART_CHAIN,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_BNB
Esempio n. 4
0
class IoTEXCovalentApi(CovalentApiBase):

    CHAIN_ID = 4689

    api_options = ApiOptions(
        blockchain=Blockchain.IOTEX,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_IOTX
Esempio n. 5
0
class KlaytnCovalentApi(CovalentApiBase):

    CHAIN_ID = 8217

    api_options = ApiOptions(
        blockchain=Blockchain.KLAYTN,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_KLAY
Esempio n. 6
0
class RskCovalentApi(CovalentApiBase):

    CHAIN_ID = 30

    api_options = ApiOptions(
        blockchain=Blockchain.RSK,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_RSK
Esempio n. 7
0
class HECOCovalentApi(CovalentApiBase):

    CHAIN_ID = 128

    api_options = ApiOptions(
        blockchain=Blockchain.HECO,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_HT
Esempio n. 8
0
class PalmCovalentApi(CovalentApiBase):

    CHAIN_ID = 11297108109

    api_options = ApiOptions(
        blockchain=Blockchain.PALM,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_PALM
Esempio n. 9
0
class MoonBeamCovalentApi(CovalentApiBase):

    CHAIN_ID = 1285

    api_options = ApiOptions(
        blockchain=Blockchain.MOONBEAM_MOONRIVER,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_MOVR
Esempio n. 10
0
class EthCovalentApi(CovalentApiBase):

    CHAIN_ID = 1

    api_options = ApiOptions(
        blockchain=Blockchain.ETHEREUM,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_ETH
Esempio n. 11
0
class AstarCovalentApi(CovalentApiBase):

    CHAIN_ID = 336

    api_options = ApiOptions(
        blockchain=Blockchain.ASTAR,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_SDN
Esempio n. 12
0
class ArbitrumCovalentApi(CovalentApiBase):

    CHAIN_ID = 42161

    api_options = ApiOptions(
        blockchain=Blockchain.ARBITRUM,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_ETH
Esempio n. 13
0
class AxieCovalentApi(CovalentApiBase):

    CHAIN_ID = 2020

    api_options = ApiOptions(
        blockchain=Blockchain.AXIE,
        base_url=CovalentApiBase.API_BASE_URL,
        rate_limit=CovalentApiBase.API_BASE_RATE_LIMIT,
    )

    coin = COIN_RON
Esempio n. 14
0
class OptimismEtherscanApi(BlockchainApi, IBalance):
    """
    Optimism
    Explorer: https://ethplorer.io
    """

    coin = COIN_ETH
    api_options = ApiOptions(
        blockchain=Blockchain.OPTIMISM,
        base_url='https://api-optimistic.etherscan.io/api',
        rate_limit=0.2,  # 0.1 in case of api_key
    )

    supported_requests = {
        'get_balance':
        '?module=account&action=balance&address={address}&tag=latest&apikey={api_key}'
    }

    def __init__(self, api_key: str = ''):
        super().__init__(api_key)

    def _parse_eth_balance(self, response: Dict) -> BalanceItem:
        return BalanceItem.from_api(
            balance_raw=response.get('result', 0),
            coin=self.coin,
            raw=response,
        )

    def get_balance(self, address: str) -> List[BalanceItem]:
        # TODO: currently returns only ETH balance (not all ERC20 tokens)
        response = self.get(
            'get_balance',
            address=address,
            api_key=self.api_key,
            # API requires User-Agent.
            headers={
                'User-Agent':
                'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_5) AppleWebKit/537.36 (KHTML, '
                'like Gecko) Chrome/50.0.2661.102 Safari/537.36'
            },
        )

        return [self._parse_eth_balance(response)]

    def _opt_raise_on_other_error(self, response: Response) -> None:
        json_response = response.json()
        if json_response["message"] == "OK":
            return

        raise ApiException(json_response['result'])
Esempio n. 15
0
class EthplorerApi(BlockchainApi, IBalance):
    """
    Ethereum
    API docs: https://github.com/EverexIO/Ethplorer/wiki/Ethplorer-API
    Explorer: https://ethplorer.io
    """

    coin = COIN_ETH
    api_options = ApiOptions(
        blockchain=Blockchain.ETHEREUM,
        base_url='https://api.ethplorer.io',
        rate_limit=0.2,  # 0.1 in case of api_key
    )

    supported_requests = {'get_info': '/getAddressInfo/{address}?apiKey={api_key}'}

    def __init__(self, api_key: str = 'freekey'):
        super().__init__(api_key)

    def get_balance(self, address: str) -> List[BalanceItem]:
        response = self.get('get_info', address=address, api_key=self.api_key)

        balances = []

        _eth = self._parse_eth_balance(response)
        if _eth is not None:
            balances.append(_eth)

        balances.extend(list(self._parse_token_balances(response)))

        return balances

    def _parse_eth_balance(self, response: Dict) -> Optional[BalanceItem]:
        _eth_raw = response['ETH']
        if int(_eth_raw['rawBalance']) == 0:
            return

        return BalanceItem.from_api(
            balance_raw=_eth_raw['rawBalance'],
            coin=self.coin,
            last_updated=None,
            raw=_eth_raw,
        )

    def _parse_token_balances(self, response: Dict) -> Iterable[BalanceItem]:
        for _token_raw in response.get('tokens', []):
            if _token_raw.get('rawBalance') is None or _token_raw['rawBalance'] == 0:
                continue

            info = _token_raw['tokenInfo']
            coin = Coin.from_api(
                blockchain=self.api_options.blockchain,
                decimals=info.get('decimals', 0),
                symbol=info.get('symbol'),
                name=info.get('name'),
                address=to_checksum_address(info['address']),
                standards=None,  # parse from tags?
                info=CoinInfo.from_api(
                    tags=info.get('publicTags'),
                    total_supply=info.get('totalSupply'),
                    logo_url=(
                        self._format_logo_url(info.get('image'))
                        if info.get('image')
                        else None
                    ),
                    coingecko_id=info.get('coingecko'),
                    website=info.get('website'),
                ),
            )

            yield BalanceItem.from_api(
                balance_raw=_token_raw['rawBalance'],
                coin=coin,
                last_updated=info.get('lastUpdated'),
                raw=_token_raw,
            )

    @staticmethod
    def _format_logo_url(raw_url: str) -> str:
        return f'https://ethplorer.io{raw_url}'
Esempio n. 16
0
class TerraFcdApi(BlockchainApi):
    """
    Terra Money FCD
    API docs: https://fcd.terra.dev/swagger
    """

    coin = COIN_TERRA
    api_options = ApiOptions(
        blockchain=Blockchain.TERRA,
        base_url='https://fcd.terra.dev/',
    )

    supported_requests = {
        'get_native_balances': '/v1/bank/{address}',
        'get_ibc_denom_trace': '/ibc/apps/transfer/v1/denom_traces/{hash}',
        'get_staking_data': '/v1/staking/{address}',
    }

    def get_native_balances(self, address: str) -> List[BalanceItem]:
        response = self.get('get_native_balances', address=address)

        balances = []
        for b in response['balance']:
            if int(b['available']) == 0:
                continue

            coin = (self._get_terra_token_by_denom(b['denom'])
                    if b['denom'].startswith('u') else
                    self._get_ibc_token_by_denom(b['denom']))

            balances.append(
                BalanceItem.from_api(balance_raw=b['available'],
                                     coin=coin,
                                     raw=b))

        return balances

    def get_staking_balances(self, address: str) -> List[BalanceItem]:
        response = self.get('get_staking_data', address=address)

        balances = []
        if int(response['delegationTotal']) > 0:
            balances.append(
                BalanceItem.from_api(
                    balance_raw=response['delegationTotal'],
                    coin=self.coin,
                    asset_type=AssetType.STAKED,
                    raw=response,
                ))

        if float(response['rewards']['total']) > 0:
            balances.append(
                BalanceItem.from_api(
                    balance_raw=response['rewards']['total'],
                    coin=self.coin,
                    asset_type=AssetType.CLAIMABLE,
                    raw=response,
                ))

        # process undelegations

        return balances

    # It's possible to get cw20 balances, but it needs to be done one by one.
    # Use .terra_mantle.py for that
    # def get_cw20_balances(self):

    @staticmethod
    def _get_terra_token_by_denom(denom: str) -> Coin:
        if denom == 'uluna':
            return COIN_TERRA
        else:
            symbol = f'{denom[1:3].upper()}T'
            return Coin.from_api(
                symbol=symbol,
                name=symbol,
                decimals=6,
                blockchain=Blockchain.TERRA,
                address=denom,
                standards=['terra-native'],
            )

    @lru_cache(maxsize=8)
    def _get_ibc_token_by_denom(self, denom: str) -> Coin:
        hash_ = denom.split('/')[1]
        try:
            response = self.get('get_ibc_denom_trace', hash=hash_)
        except ApiException:
            # add log
            symbol = None
        else:
            denom = response['denom_trace']['base_denom']
            symbol = denom[1:].upper()

        return Coin.from_api(
            symbol=symbol,
            name=symbol,
            decimals=6,
            blockchain=Blockchain.TERRA,
            address=hash_,
            standards=['ibc'],
        )
Esempio n. 17
0
class TerraMantleApi(BlockchainApi):
    """
    Terra Money Subgraph API
    API docs: https://mantle.terra.dev
    """

    coin = COIN_TERRA
    api_options = ApiOptions(
        blockchain=Blockchain.TERRA,
        base_url='https://mantle.terra.dev',
    )

    # API uses post requests
    supported_requests = {}
    _post_requests = {
        'wasm_contract_address_store':
        """
            WasmContractsContractAddressStore(
                ContractAddress: "$CONTRACT_ADDRESS",
                QueryMsg: "$QUERY_MSG"
            ){
                Result
            }
        """
    }

    _tokens_map: Optional[Dict[str, Dict]] = None

    @property
    def tokens_map(self) -> Dict[str, Dict]:
        if self._tokens_map is None:
            response = self._session.get(
                'https://assets.terra.money/cw20/tokens.json')
            token_list = response.json()
            self._tokens_map = token_list['mainnet']

        return self._tokens_map

    def get_cw20_balances(self, address: str):
        raw_balances = self._get_raw_balances(address)

        balances = []
        for contract, result_raw in raw_balances['data'].items():
            data_raw = json.loads(result_raw['Result'])
            balance_raw = data_raw['balance']
            if int(balance_raw) == 0:
                continue

            balances.append(
                BalanceItem.from_api(
                    balance_raw=balance_raw,
                    coin=self._get_token_data(contract),
                    raw=result_raw,
                ))

        return balances

    def _get_token_data(self, address: str) -> Coin:
        raw_token = self.tokens_map[address]
        return Coin(
            symbol=raw_token['symbol'],
            name=raw_token['name']
            if raw_token.get('name') else raw_token['symbol'],
            decimals=6,
            blockchain=Blockchain.TERRA,
            address=address,
            standards=['CW20'],
            protocol=raw_token.get('protocol'),
            info=CoinInfo.from_api(logo_url=raw_token.get('icon')),
        )

    def _get_raw_balances(self, address: str) -> Dict:
        cw20_contracts = list(self.tokens_map.keys())
        message = '{\\"balance\\": {\\"address\\": \\"$ADDR\\"}}'.replace(
            '$ADDR', address)

        key_queries = [
            self._create_key_query(
                key=contract,
                query=self._build_query(
                    method='wasm_contract_address_store',
                    params={
                        '$CONTRACT_ADDRESS': contract,
                        '$QUERY_MSG': message
                    },
                ),
            ) for contract in cw20_contracts
        ]
        query = self._concat_key_queries(key_queries)
        return self.post(json={'query': query})

    def _build_query(self,
                     method: str,
                     params: Optional[Dict[str, str]] = None) -> str:
        query = self._post_requests.get(method)
        if params:
            for k, v in params.items():
                query = query.replace(k, v)
        return query

    @staticmethod
    def _create_key_query(key: str, query: str) -> str:
        return f'{key}: {query}'

    @staticmethod
    def _concat_key_queries(key_queries: Sequence[str]) -> str:
        return '{' + ',\n'.join(key_queries) + '}'

    def _opt_raise_on_other_error(self, response: Response) -> None:
        json_response = response.json()
        if json_response.get('errors') is None:
            return

        # pick first message
        err = json_response['errors'][0]

        if 'addr_canonicalize' in err['message']:
            raise InvalidAddressException(f'Invalid address format.')
Esempio n. 18
0
class SolanaApi(BlockchainApi, IBalance):
    """
    Solana RPC
    API docs: https://docs.solana.com/apps/jsonrpc-api
    """

    coin = COIN_SOL
    api_options = ApiOptions(
        blockchain=Blockchain.SOLANA,
        base_url='https://api.mainnet-beta.solana.com/',
        start_offset=0,
        max_items_per_page=1000,
        page_offset_step=1,
    )

    # API uses post requests
    supported_requests = {}

    token_program_id = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'
    _tokens_map: Optional[Dict[str, Dict]] = None

    @property
    def tokens_map(self) -> Dict[str, Dict]:
        if self._tokens_map is None:
            response = self._session.get(
                'https://raw.githubusercontent.com/solana-labs/token-list/'
                'main/src/tokens/solana.tokenlist.json'
            )
            token_list = response.json()
            self._tokens_map = {t['address']: t for t in token_list['tokens']}

        return self._tokens_map

    def get_balance(self, address: str):
        balances = []

        sol_balance = self._get_sol_balance(address)
        if sol_balance is not None:
            balances.append(sol_balance)

        token_balances = list(self._yield_token_balances(address))
        if token_balances:
            balances.extend(token_balances)

        return balances

    def _get_sol_balance(
        self,
        address: str,
    ) -> Optional[BalanceItem]:
        response = self._request(method='getBalance', params=[address])
        if int(response['result']['value']) == 0:
            return

        return BalanceItem.from_api(
            balance_raw=response['result']['value'],
            coin=self.coin,
            last_updated=None,
            raw=response,
        )

    def _yield_token_balances(self, address: str) -> Iterable[BalanceItem]:
        response = self._request(
            method='getTokenAccountsByOwner',
            params=[
                address,
                {'programId': self.token_program_id},
                {'encoding': 'jsonParsed'},
            ],
        )

        for raw_balance in response['result']['value']:
            balance = self._parse_token_balance(raw_balance)
            if balance is not None:
                yield balance

    def _parse_token_balance(self, raw: Dict) -> Optional[BalanceItem]:
        info = raw['account']['data']['parsed']['info']
        if int(info['tokenAmount']['amount']) == 0:
            return

        address = info['mint']

        if address in self.tokens_map:
            token = self._get_token_data(address)
        else:
            token = Coin.from_api(
                blockchain=Blockchain.SOLANA,
                decimals=info['tokenAmount']['decimals'],
                address=address,
            )

        return BalanceItem.from_api(
            balance_raw=info['tokenAmount']['amount'],
            coin=token,
            raw=raw,
        )

    def _get_token_data(self, address: str) -> Coin:
        raw_token = self.tokens_map[address]
        extensions = raw_token.get('extensions', {})

        return Coin(
            symbol=raw_token['symbol'],
            name=raw_token['name'],
            decimals=raw_token['decimals'],
            blockchain=Blockchain.SOLANA,
            address=address,
            standards=['SPL'],
            info=CoinInfo.from_api(
                tags=raw_token.get('tags'),
                logo_url=raw_token.get('logoURI'),
                coingecko_id=extensions.get('coingeckoId'),
                website=extensions.get('website'),
            ),
        )

    def _request(self, method, params):
        body = json.dumps(
            {'jsonrpc': '2.0', 'id': 1, 'method': method, 'params': params}
        )
        return self.post(body=body, headers={'Content-Type': 'application/json'})

    def _opt_raise_on_other_error(self, response: Response) -> None:
        json_response = response.json()
        if 'error' not in json_response:
            return

        if 'Invalid param' in json_response['error']['message']:
            raise InvalidAddressException(f'Invalid address format.')
        else:
            raise ApiException(json_response['error']['message'])