def get_rewards_for_cycle(self,
                              cycle,
                              expected_reward=False,
                              verbose=False):
        #############
        root = {
            "delegate_staking_balance": 0,
            "total_reward_amount": 0,
            "delegators_balance": {}
        }

        uri = self.api['API_URL'] + rewards_split_call.format(
            self.baking_address, cycle)

        if verbose:
            logger.debug("Requesting {}".format(uri))

        resp = requests.get(uri, timeout=5)

        if verbose:
            logger.debug("Response from tzstats is {}".format(resp))

        if resp.status_code != 200:
            # This means something went wrong.
            raise ApiProviderException('GET {} {}'.format(
                uri, resp.status_code))

        resp = resp.json()[0]
        if expected_reward:
            root["total_reward_amount"] = int(
                1e6 * float(resp[idx_income_expected_income]))
        else:
            root["total_reward_amount"] = int(
                1e6 * (float(resp[idx_income_baking_income]) +
                       float(resp[idx_income_endorsing_income]) +
                       float(resp[idx_income_seed_income]) +
                       float(resp[idx_income_fees_income]) -
                       float(resp[idx_income_lost_accusation_fees]) -
                       float(resp[idx_income_lost_accusation_rewards]) -
                       float(resp[idx_income_lost_revelation_fees]) -
                       float(resp[idx_income_lost_revelation_rewards])))

        uri = self.api['API_URL'] + delegators_call.format(
            cycle - self.preserved_cycles - 2, self.baking_address)

        if verbose:
            logger.debug("Requesting {}".format(uri))

        resp = requests.get(uri, timeout=5)

        if verbose:
            logger.debug("Response from tzstats is {}".format(resp))

        if resp.status_code != 200:
            # This means something went wrong.
            raise ApiProviderException('GET {} {}'.format(
                uri, resp.status_code))

        resp = resp.json()

        for delegator in resp:
            if delegator[idx_delegator_address] == self.baking_address:
                root["delegate_staking_balance"] = int(
                    1e6 * (float(delegator[idx_baker_balance]) +
                           float(delegator[idx_baker_delegated])))
            else:
                root["delegators_balance"][
                    delegator[idx_delegator_address]] = int(
                        1e6 * float(delegator[idx_delegator_balance]))

        return root
Example #2
0
    def __get_delegators_and_delgators_balances(self, cycle, current_level):

        # calculate the hash of the block for the chosen snapshot of the rewards cycle
        roll_snapshot, level_snapshot_block = self.__get_roll_snapshot_block_level(
            cycle, current_level)
        if level_snapshot_block == "":
            raise ApiProviderException(
                "[get_d_d_b] level_snapshot_block is empty. Unable to proceed."
            )
        if roll_snapshot < 0 or roll_snapshot > 15:
            raise ApiProviderException(
                "[get_d_d_b] roll_snapshot is outside allowable range: {} Unable to proceed."
                .format(roll_snapshot))

        # construct RPC for getting list of delegates and staking balance
        get_delegates_request = COMM_DELEGATES.format(self.node_url,
                                                      level_snapshot_block,
                                                      self.baking_address)

        delegate_staking_balance = 0
        d_a_len = 0
        delegators = {}

        try:
            # get RPC response for delegates and staking balance
            response = self.do_rpc_request(get_delegates_request)
            delegate_staking_balance = int(response["staking_balance"])

            # If roll_snapshot == 15, we need to adjust the baker's staking balance
            # by subtracting unfrozen rewards due to when the snapshot is taken
            # within the block context. For more information, see:
            # https://medium.com/@_MisterWalker_/we-all-were-wrong-baking-bad-and-most-bakers-were-using-wrong-data-to-calculate-staking-rewards-a8c26f5ec62b
            if roll_snapshot == 15:
                if cycle >= FIRST_CYCLE_REWARDS_GRANADA:
                    # Since cycle 394, we use an offset of 1589248 blocks (388 cycles pre-Granada of 4096 blocks each)
                    # Cycles start at 0.
                    old_rewards_cycle = (CYCLES_BEFORE_GRANADA + (
                        (level_snapshot_block - BLOCKS_BEFORE_GRANADA) /
                        self.blocks_per_cycle) - self.preserved_cycles - 1)
                else:
                    old_rewards_cycle = ((level_snapshot_block /
                                          BLOCKS_PER_CYCLE_BEFORE_GRANADA) -
                                         self.preserved_cycles - 1)
                _, unfrozen_rewards = self.__get_unfrozen_rewards(
                    level_snapshot_block, old_rewards_cycle)
                delegate_staking_balance -= unfrozen_rewards

            # Remove baker's address from list of delegators
            delegators_addresses = list(
                filter(lambda x: x != self.baking_address,
                       response["delegated_contracts"]))
            d_a_len = len(delegators_addresses)

            if d_a_len == 0:
                raise ApiProviderException("[get_d_d_b] No delegators found")

            # Loop over delegators; get snapshot balance, and current balance
            for idx, delegator in enumerate(delegators_addresses):
                # create new dictionary for each delegator
                d_info = {"staking_balance": 0, "current_balance": 0}

                get_staking_balance_request = COMM_DELEGATE_BALANCE.format(
                    self.node_url, level_snapshot_block, delegator)
                d_info["staking_balance"] = int(
                    self.__get_response(delegator,
                                        get_staking_balance_request))

                sleep(
                    0.5
                )  # Be nice to public RPC since we are now making 2x the amount of RPC calls

                d_info[
                    "current_balance"] = self.__get_current_balance_of_delegator(
                        delegator)

                logger.debug(
                    "Delegator info ({}/{}) fetched: address {}, staked balance {}, current balance {} "
                    .format(
                        idx + 1,
                        d_a_len,
                        delegator,
                        d_info["staking_balance"],
                        d_info["current_balance"],
                    ))
                if idx % 10 == 0:
                    logger.info("Delegator info ({}/{}) fetched.".format(
                        idx + 1, d_a_len))

                # "append" to master dict
                delegators[delegator] = d_info

        except ApiProviderException as r:
            logger.error("[get_d_d_b] RPC API Error: {}".format(str(r)))
            raise r from r
        except Exception as e:
            logger.error("[get_d_d_b] Unexpected error: {}".format(str(e)),
                         exc_info=True)
            raise e from e

        # Sanity check. We should have fetched info for all delegates. If we didn't, something went wrong
        d_len = len(delegators)
        if d_a_len != d_len:
            raise ApiProviderException(
                "[get_d_d_b] Did not collect info for all delegators, {}/{}".
                format(d_a_len, d_len))

        return delegate_staking_balance, delegators
    def __get_delegators_and_delgators_balances(self, cycle, current_level):

        # calculate the hash of the block for the chosen snapshot of the rewards cycle
        roll_snapshot, level_snapshot_block = self.__get_roll_snapshot_block_level(cycle, current_level)
        if level_snapshot_block == "":
            raise ApiProviderException("[get_d_d_b] level_snapshot_block is empty. Unable to proceed.")
        if roll_snapshot < 0 or roll_snapshot > 15:
            raise ApiProviderException("[get_d_d_b] roll_snapshot is outside allowable range: {} Unable to proceed.".format(roll_snapshot))

        # construct RPC for getting list of delegates and staking balance
        get_delegates_request = COMM_DELEGATES.format(self.node_url, level_snapshot_block, self.baking_address)

        delegate_staking_balance = 0
        d_a_len = 0
        delegators = {}

        try:
            # get RPC response for delegates and staking balance
            response = self.do_rpc_request(get_delegates_request)
            delegate_staking_balance = int(response["staking_balance"])

            # If roll_snapshot == 15, we need to adjust the baker's staking balance
            # by subtracting unfrozen rewards due to when the snapshot is taken
            # within the block context
            if roll_snapshot == 15:
                old_rewards_cycle = (level_snapshot_block / self.blocks_per_cycle) - self.preserved_cycles - 1
                _, unfrozen_rewards = self.__get_unfrozen_rewards(level_snapshot_block, old_rewards_cycle)
                delegate_staking_balance -= unfrozen_rewards

            # Remove baker's address from list of delegators
            delegators_addresses = list(filter(lambda x: x != self.baking_address, response["delegated_contracts"]))
            d_a_len = len(delegators_addresses)

            if d_a_len == 0:
                raise ApiProviderException("[get_d_d_b] No delegators found")

            # Loop over delegators; get snapshot balance, and current balance
            for idx, delegator in enumerate(delegators_addresses):
                # create new dictionary for each delegator
                d_info = {"staking_balance": 0, "current_balance": 0}

                get_staking_balance_request = COMM_DELEGATE_BALANCE.format(self.node_url, level_snapshot_block,
                                                                           delegator)

                staking_balance_response = None

                while not staking_balance_response:
                    try:
                        staking_balance_response = self.do_rpc_request(get_staking_balance_request, time_out=5)
                    except Exception as e:
                        logger.debug("[get_d_d_b] Fetching delegator {:s} staking balance failed: {:s}, will retry".format(delegator, str(e)))
                        sleep(1.0)  # Sleep between failure

                d_info["staking_balance"] = int(staking_balance_response)

                sleep(0.5)  # Be nice to public RPC since we are now making 2x the amount of RPC calls

                d_info["current_balance"] = self.__get_current_balance_of_delegator(delegator)

                logger.debug("Delegator info ({}/{}) fetched: address {}, staked balance {}, current balance {} "
                             .format(idx + 1, d_a_len, delegator, d_info["staking_balance"], d_info["current_balance"]))

                # "append" to master dict
                delegators[delegator] = d_info

        except ApiProviderException as r:
            logger.error("[get_d_d_b] RPC API Error: {}".format(str(r)))
            raise r from r
        except Exception as e:
            logger.error("[get_d_d_b] Unexpected error: {}".format(str(e)), exc_info=True)
            raise e from e

        # Sanity check. We should have fetched info for all delegates. If we didn't, something went wrong
        d_len = len(delegators)
        if d_a_len != d_len:
            raise ApiProviderException("[get_d_d_b] Did not collect info for all delegators, {}/{}".format(d_a_len, d_len))

        return delegate_staking_balance, delegators
    def get_rewards_for_cycle(self, cycle, expected_reward=False):

        root = {"delegate_staking_balance": 0, "total_reward_amount": 0, "delegators_balances": {}}

        #
        # Get rewards breakdown for cycle
        #
        uri = self.api['API_URL'] + rewards_split_call.format(self.baking_address, cycle)

        sleep(0.5)  # be nice to tzstats

        verbose_logger.debug("Requesting rewards breakdown, {}".format(uri))

        resp = requests.get(uri, timeout=5)

        verbose_logger.debug("Response from tzstats is {}".format(resp.content.decode("utf8")))

        if resp.status_code != 200:
            # This means something went wrong.
            raise ApiProviderException('GET {} {}'.format(uri, resp.status_code))

        resp = resp.json()[0]
        if expected_reward:
            root["total_reward_amount"] = int(1e6 * float(resp[idx_income_expected_income]))
        else:
            root["total_reward_amount"] = int(1e6 * (float(resp[idx_income_baking_income])
                                                     + float(resp[idx_income_endorsing_income])
                                                     + float(resp[idx_income_seed_income])
                                                     + float(resp[idx_income_fees_income])
                                                     - float(resp[idx_income_lost_accusation_fees])
                                                     - float(resp[idx_income_lost_accusation_rewards])
                                                     - float(resp[idx_income_lost_revelation_fees])
                                                     - float(resp[idx_income_lost_revelation_rewards])))

        #
        # Get staking balances of delegators at snapshot block
        #
        uri = self.api['API_URL'] + delegators_call.format(cycle - self.preserved_cycles - 2, self.baking_address)

        sleep(0.5)  # be nice to tzstats

        verbose_logger.debug("Requesting staking balances of delegators, {}".format(uri))

        resp = requests.get(uri, timeout=5)

        verbose_logger.debug("Response from tzstats is {}".format(resp.content.decode("utf8")))

        if resp.status_code != 200:
            # This means something went wrong.
            raise ApiProviderException('GET {} {}'.format(uri, resp.status_code))

        resp = resp.json()

        for delegator in resp:

            if delegator[idx_delegator_address] == self.baking_address:
                root["delegate_staking_balance"] = int(
                    1e6 * (float(delegator[idx_balance]) + float(delegator[idx_baker_delegated])))
            else:
                delegator_info = {"staking_balance": 0, "current_balance": 0}
                delegator_info["staking_balance"] = int(1e6 * float(delegator[idx_balance]))
                root["delegators_balances"][delegator[idx_delegator_address]] = delegator_info

        #
        # Get current balance of delegates
        #
        # This is done in 2 phases. 1) make a single API call to tzstats, retrieving an array
        # of arrays with current balance of each delegator who "currently" delegates to delegate. There may
        # be a case where the delegator has changed delegations and would therefor not be in this array.
        # Thus, 2) determines which delegators are not in the first result, and makes individual
        # calls to get their balance. This approach should reduce the overall number of API calls made to tzstats.
        #

        # Phase 1
        #
        uri = self.api['API_URL'] + batch_current_balance_call.format(self.baking_address)

        sleep(0.5)  # be nice to tzstats

        verbose_logger.debug("Requesting current balance of delegators, phase 1, {}".format(uri))

        resp = requests.get(uri, timeout=5)

        verbose_logger.debug("Response from tzstats is {}".format(resp.content.decode("utf8")))

        if resp.status_code != 200:
            # This means something went wrong.
            raise ApiProviderException('GET {} {}'.format(uri, resp.status_code))

        resp = resp.json()

        # Will use these two lists to determine who has/has not been fetched
        staked_bal_delegators = root["delegators_balances"].keys()
        curr_bal_delegators = []

        for delegator in resp:
            delegator_addr = delegator[idx_cb_delegator_address]

            # If delegator is in this batch, but has no staking balance for this reward cycle,
            # then they must be a new delegator and are not receiving rewards at this time.
            # We can ignore them.
            if delegator_addr not in staked_bal_delegators:
                continue

            root["delegators_balances"][delegator_addr]["current_balance"] = int(
                1e6 * float(delegator[idx_cb_current_balance]))
            curr_bal_delegators.append(delegator_addr)

        # Phase 2
        #

        # Who was not in this result?
        need_curr_balance_fetch = list(set(staked_bal_delegators) - set(curr_bal_delegators))

        # Fetch individual not in original batch
        if len(need_curr_balance_fetch) > 0:
            split_addresses = split(need_curr_balance_fetch, 50)
            for list_address in split_addresses:
                list_curr_balances = self.__fetch_current_balance(list_address)
                for d in list_address:
                    root["delegators_balances"][d]["current_balance"] = list_curr_balances[d]
                    curr_bal_delegators.append(d)

        # All done fetching balances.
        # Sanity check.
        n_curr_balance = len(curr_bal_delegators)
        n_stake_balance = len(staked_bal_delegators)

        if n_curr_balance != n_stake_balance:
            raise ApiProviderException('Did not fetch all balances {}/{}'.format(n_curr_balance, n_stake_balance))

        return root