text: import re
  from copy import deepcopy
  
  import dateutil.parser
  
  from blockapi.services import (
      BlockchainAPI,
      APIError,
      set_default_args_values,
      AddressNotExist
  )
  
  
  class CosmosAPI(BlockchainAPI):
      """
      Cosmos
      API docs: https://cosmos.network/rpc/
      Explorer: https://www.mintscan.io
      """
  
      symbol = 'ATOM'
      # base_url is 'https://stargate.cosmos.network'
      base_url = 'https://cosmos.node.cracklord.com'
      rate_limit = 0.6
      coef = 1e-6
      start_offset = 1
      max_items_per_page = 30
      page_offset_step = 1
  
      supported_requests = {
          'get_info': '/auth/accounts/{address}',
          'get_balance': '/bank/balances/{address}',
          'get_txs': '/txs?action={action}&{role}={address}&page={page}'
                     '&limit={limit}',
          'get_delegations': '/staking/delegators/{address}/delegations',
          'get_unbonding_delegations': '/staking/delegators/{address}'
                                       '/unbonding_delegations',
          'get_redelegations': '/staking/redelegations?delegator={address}',
          'get_delegation_reward': '/distribution/delegators/{address}'
                                   '/rewards/{validator_address}',
          'get_proposals': '/gov/proposals',
          'query_votes': '/gov/proposals/{proposal_id}/votes',
      }
  
      def process_error_response(self, response):
          if 'decoding bech32 failed' in response.text:
              raise AddressNotExist()
          # else
          super().process_error_response(response)
  
      def get_info(self):
          return self.request('get_info', address=self.address)
  
      def get_balance(self):
          balances = self.request('get_balance', address=self.address)
          if not balances:
              return None
  
          balances_result = []
          for b in balances['result']:
              symbol = self.symbol if b['denom'] == 'uatom' else b['denom']
              balances_result.append({
                  'symbol': symbol,
                  'amount': int(b['amount']) * self.coef
              })
          return balances_result
  
      def get_delegations(self):
          return self.request('get_delegations', address=self.address)
  
      def get_unbonding_delegations(self):
          return self.request('get_unbonding_delegations', address=self.address)
  
      def get_redelegations(self):
          return self.request('get_redelegations', address=self.address)
  
      def get_delegation_reward(self, validator_address):
          return self.request('get_delegation_reward',
                              address=self.address,
                              validator_address=validator_address)
  
      def get_proposals(self):
          return self.request('get_proposals')
  
      def get_votes_deprecated(self):
          votes = []
          for prop in self.get_proposals():
              try:
                  vote = self.request('query_votes',
                                      proposal_id=prop['proposal_id'])
                  if vote is None:
                      continue
                  for v in vote:
                      if v['voter'] == self.address:
                          votes.append(v)
  
              except APIError:
                  pass
  
          return votes
  
      def get_incoming_txs(self, offset=None, limit=None, unconfirmed=False):
          txs = self._get_txs('send', 'recipient', offset, limit, unconfirmed)
          return self.parse_txs(txs)
  
      def get_outgoing_txs(self, offset=None, limit=None, unconfirmed=False):
          txs = self._get_txs('send', 'sender', offset, limit, unconfirmed)
          return self.parse_txs(txs)
  
      def get_multi_incoming_txs(self, offset=None, limit=None,
                                 unconfirmed=False):
          txs = self._get_txs('multisend', 'recipient', offset, limit,
                              unconfirmed)
          return self.parse_txs(txs)
  
      def get_multi_outgoing_txs(self, offset=None, limit=None,
                                 unconfirmed=False):
          txs = self._get_txs('multisend', 'sender', offset, limit, unconfirmed)
          return self.parse_txs(txs)
  
      def parse_txs(self, txs):
          parsed_txs = []
          for tx in txs['txs']:
              # there can be multiple txs in single tx
              for parsed in self.parse_tx(tx):
                  parsed_txs.append(parsed)
          return parsed_txs
  
      def parse_tx(self, tx):
          fee_amount = int(tx['tx']['value']['fee']['amount'][0]['amount'])
          base_tx = {
              'date': dateutil.parser.parse(tx['timestamp']),
              'fee': fee_amount * self.coef if fee_amount else None,
              'gas': {
                  'gas_used': int(tx['gas_used']),
                  'gas_limit': int(tx['gas_wanted'])
              },
              'hash': tx['txhash'],
              'confirmed': None,
              'type': 'normal',
              'kind': 'transaction',
              'description': tx['tx']['value']['memo'],
              'raw': tx
          }
  
          # for every message (sub tx?) create new tx
          msgs = []
          for msg in self._parse_messages(tx):
              msg['direction'] = (
                  'incoming'
                  if msg['to_address'].lower() == self.address.lower()
                  else 'outgoing'
              )
              msg.update(base_tx)
              msgs.append(msg)
          return msgs
  
      #
      def get_rewards_withdrawals(self, offset=None, limit=None):
          items = self._get_txs('withdraw_delegator_reward', 'delegator', offset,
                                limit)
          return self.parse_other_txs(items)
  
      #
      def get_redelegates(self, offset=None, limit=None):
          items = self._get_txs('begin_redelegate', 'delegator', offset, limit)
          return self.parse_other_txs(items)
  
      #
      def get_delegates(self, offset=None, limit=None):
          items = self._get_txs('delegate', 'delegator', offset, limit)
          return self.parse_other_txs(items)
  
      #
      def get_undelegates(self, offset=None, limit=None):
          items = self._get_txs('begin_unbonding', 'delegator', offset, limit)
          return self.parse_other_txs(items)
  
      def get_votes(self, offset=None, limit=None):
          items = self._get_txs('vote', 'voter', offset, limit)
          return self.parse_other_txs(items)
  
      # only for validators
      def get_validator_edits(self, offset=None, limit=None):
          items = self._get_txs('edit_validator', 'destination-validator',
                                offset, limit)
          return self.parse_other_txs(items)
  
      # create validator
      # https://stargate.cosmos.network/txs/938AFA947B6EBE4CA614FFD7C8F98F2452
      # 8A395061FAC68EF1EFB92422F6158F
  
      def parse_other_txs(self, txs):
          parsed_txs = []
          for tx in txs:
              # there can be multiple txs in single tx
              for parsed in self._parse_other_tx(tx):
                  parsed_txs.append(parsed)
          return parsed_txs
  
      @set_default_args_values
      def _get_txs(self, action, role, offset=None, limit=None,
                   unconfirmed=None):
          return self.request(
              'get_txs',
              action=action,
              role=role,
              address=self.address,
              page=offset,
              limit=limit,
              unconfirmed=unconfirmed
          )
  
      def _parse_other_tx(self, tx):
          fee_data = tx['tx']['value']['fee']['amount']
          fee_amount = int(fee_data[0]['amount']) * self.coef if fee_data \
              else None
  
          base_tx = {
              'date': dateutil.parser.parse(tx['timestamp']),
              'fee': fee_amount,
              'gas': {
                  'gas_used': int(tx['gas_used']),
                  'gas_limit': int(tx['gas_wanted'])
              },
              'hash': tx['txhash'],
              'description': tx['tx']['value']['memo'],
              'raw': tx
          }
  
          # for every message (sub tx?) create new tx
          msgs = []
          for msg in self._parse_messages(tx):
              _tx = deepcopy(base_tx)
              _tx['kind'] = msg.pop('kind')
              _tx['operation'] = msg
              msgs.append(_tx)
          return msgs
  
      def _parse_messages(self, tx):
          logs = deepcopy(tx['logs'])
  
          # only reward has values in separated dict
          r = re.compile("([0-9]+)([a-zA-Z]+)")
          # [($number, $coin), ...]
          rewards = [r.match(t['value']).groups()
                     for t in tx['tags'] if t['key'] == 'rewards']
  
          parsed_msgs = []
          for msg in tx['tx']['value']['msg']:
  
              msg_type = self._convert_msg_kind(msg['type'])
              parsed_msg = self._get_msg_data(msg_type, msg['value'])
  
              if msg_type == 'reward':
                  reward = rewards.pop(0) if rewards else None
                  if reward:
                      parsed_msg['amount'] = int(reward[0]) * self.coef
  
              log = logs.pop(0)
              parsed_msg['is_error'] = not log['success']
              parsed_msg['kind'] = msg_type
  
              parsed_msgs.append(parsed_msg)
  
          return parsed_msgs
  
      @staticmethod
      def _convert_msg_kind(raw_type):
          return {
              'cosmos-sdk/MsgSend': 'transaction',
              'cosmos-sdk/MsgMultiSend': 'multisend',
              'cosmos-sdk/MsgWithdrawDelegationReward': 'reward',
              'cosmos-sdk/MsgDelegate': 'delegate',
              'cosmos-sdk/MsgBeginRedelegate': 'redelegate',
              'cosmos-sdk/MsgVote': 'vote'
          }.get(raw_type, raw_type)
  
      def _get_msg_data(self, msg_type, msg_value):
          # use original fields from api, change only amounts if needed
          msg_info = deepcopy(msg_value)
  
          if msg_type == 'reward':
              # amount for rewards are saved in different field,
              # see caller method
              pass
  
          elif msg_type == 'multisend':
              msg_result = self._process_msg_type_multisend(msg_value)
              if msg_result is not None:
                  return msg_result
          else:
              amount_obj = msg_value['amount']
              amount_data = {}
              if isinstance(amount_obj, list):
                  amount_data = amount_obj[0] if amount_obj else {}
              elif isinstance(amount_obj, dict):
                  amount_data = amount_obj
  
              msg_info['amount'] = (int(amount_data['amount']) * self.coef
                                    if amount_data else None)
  
          return msg_info
  
      def _process_msg_type_multisend(self, msg_value):
          my_input = next((i for i in msg_value['inputs']
                           if i['address'].lower() == self.address.lower()),
                          None)
  
          my_output = next((i for i in msg_value['outputs']
                            if i['address'].lower() == self.address.lower()),
                           None)
  
          if my_input:
              to_address = (msg_value['outputs'][0]['address']
                            if len(msg_value['outputs']) == 1
                            else 'multiple')
              return {
                  'from_address': self.address,
                  'to_address': to_address,
                  'amount': int(my_input['coins'][0]['amount']) * self.coef
              }
          if my_output:
              from_address = (msg_value['inputs'][0]['address']
                              if len(msg_value['inputs']) == 1
                              else 'multiple')
              return {
                  'from_address': from_address,
                  'to_address': self.address,
                  'amount': int(my_output['coins'][0]['amount']) * self.coef
              }
  