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
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
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
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
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
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
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
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
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
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
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
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
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
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'])
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}'
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'], )
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.')
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'])