[To Hive]Estimate author payouts with Steem.js

in HiveDevs4 years ago (edited)

image.png

What Will I Learn?

In this tutorial,

  • You will learn, how a payout for a post on the hive blockchain is processed, and
  • You will learn, how to rebuild this computations with Steem.js and Node.js.

Requirements

To follow this tutorial, you need

  • Experience in JavaScript and Node.js,
  • Basic knowledge on the hive blockchain,
  • and a working Node.js installation and development environment.

Difficulty

Intermediate

Tutorial Contents
This tutorial explains how the author rewards for a post on the HIVE blockchain are computed and how to rebuild this computation with Steem.js.
With this tutorial, you will be able to predict the author payouts in HIVE, HBD, and HIVE power for a post more accurately than the popup on hive, which does not distinguish between author/curation/beneficiary rewards.


image.png

Setup a Node.js project with Steem.js

  • create a new Node.js project and install Steem.js with
npm init
npm install steem --save
  • and create a new JavaScript source file. E.g. payout.js and import Steem.js with
const steem = require("steem");

You can find the full source code for payout.js on GitHub. - lets turn it to hive!

Functions to get a post, the reward fund, the dynamic global properties, and the current price of hive

In order to compute the author rewards for a post, we need to retrieve four objects from the hive blockchain with Steem.js. The post (of course), the reward balance, the dynamic global properties, and the current hive price from the median price history.

There are several ways to retrieve posts from the blockchain, here I will use getContent() which returns a post object given the account name of the author and the permlink of the post. To ease the handling of multiple Steem.js API calls, I wrap them into Promise objects with small wrapper functions.

var getContent = async function(author, permlink) {
  return new Promise((resolve, reject) => {
    steem.api.getContent(author, permlink, function(err, result) {
      if (err) reject(err);
      resolve(result);
    });
  });
};

The reward fund contains the amount of STEEM available for author and curation rewards, which is generated by the inflation model built into the STEEM blockchain. It also contains the number of recent claims, which determines the share of the reward fund per VESTS (also known as STEEM power) put into the votes for a post. We retrieve it with the following wrapper

var getRewardFund = async function() {
  return new Promise((resolve, reject) => {
    steem.api.getRewardFund("post", (err, result) => {
      if (err) reject(err);
      resolve(result);
    });
  });
};

The dynamic global properties contain exactly one value required to compute author rewards: HBD_payout_rate. When a new post is created, one must specify, how the payout should be split in HBD and HBD power. Typically a 50%/50% split is selected. However, of these 50% only the percentage determined by hbd_payout_rate is payed in hbd, while the remaining share is payed in hive. The hbd_payout_rate is a mechanism to keep the market cap of HBD at a reasonable relation to the market cap of hive. If this relation rises above 2%, hbd_payout_rate is reduced.

var getDynamicGlobalProperties = async function() {
  return new Promise((resolve, reject) => {
    steem.api.getDynamicGlobalProperties((err, result) => {
      if (err) reject(err);
      resolve(result);
    });
  });
};

Finally for the conversion between HBD and hive, we need the current median history price, which is the median of the hive price over the last three and a half days. As the median price fluctuates, the author payout in HBD computed by this tutorial fluctuates as well (the share which is credited as hive power is not affected by this fluctuations).

var getCurrentMedianHistoryPrice = function() {
  return new Promise((resolve, reject) => {
    steem.api.getCurrentMedianHistoryPrice((err, response) => {
      if (err) reject(err);
      resolve(response);
    });
  });
};

1. Compute the total reward for a post

The function get_rshare_reward() to get the total reward in a post.
The comment_reward_context has been populated (i.a.) with The comment_reward_context has been populated (i.a.) with post.net_rshares (in ctx.rshares), reward_fund.recent_claims (in ctx.total_reward_shares2), reward_fund.reward_balance (in ctx.total_reward_fund_steem), and post.reward_weight (in ctx.reward_weight).

The value in post.net_rshares accumulates the rshares of all votes on the post. The rshares of a vote are given by 0.02 * vote_percentage * voting_power * vesting_shares, where vesting_shares corresponds to the hive Power of the voter.

As you can see in get_rshare_reward(), this value is transformed with a function specified in ctxt.reward_curve. For author rewards this function is just linear and can thus be omitted here. It's sufficient to multiply post.net_rshares with the percentage in reward_weight. Percentages in hive are always represented as integer values between -10000 (-100.0%) and 10000 (100.0%). Hence, reward_weight must be divided by 10000.

The result of this operation is claim, which directly results in the reward by multiplying it with reward_balance and dividing it by recent_claim<s (btw., this claim is added to recent_claims in the current hive block). hive uses fixed point arithmetics everywhere. Amounts in hive are computed with three fractional digits. As the number type in JavaScript only supports doubles, this is emulated by rounding the result down with a precision of three fractional digits. Be aware, that this may introduce slight deviations from the values computed on the hive blockchain.

The unit of reward is hive, as it is derived from reward_balance.

Posts with a total reward of less than 0.020 hbd aren't rewarded at all. is_comment_payout_dust() is called by get_rshare_reward() to check this. Here we simply multiply the reward with the hive_price and return 0 if the result is less than 0.02. Finally the reward is clipped with post.max_accepted_payout (which is given in hbd so it must be divided by the hbd_price to match the unit of reward).

ar get_rshare_reward = function(
  post,
  recent_claims,
  reward_balance,
  hive_price
) {
  const claim = post.net_rshares * post.reward_weight / 10000.0;
  const reward =
    Math.floor(1000.0 * claim * reward_balance / recent_claims) / 1000.0;

  // There is a payout threshold of 0.020 HBD.
  if (reward * hive_price < 0.02) return 0;

  const max_hive =
    parseFloat(post.max_accepted_payout.replace(" HBD", "")) / hive_price;

  return Math.min(reward, max_hive);
};

2. Reward all curators

After the total reward has been computed, all votes on the post receive their curation reward with the helper function. The second parameter to this function is the maximum amount of reward for curation. The percentage for curation rewards is stored in reward_fund.percent_curation_rewards (currently 50%), such that the maximum number of curation tokens can be derived with

const curation_tokens =
  reward * reward_fund.percent_curation_rewards / 10000.0;

Not all hive allocated in curation_tokens may be spent as curation reward, since votes within the first 5 minutes after the creation of a post receive less curation rewards. The pay_curators function successively reduces the passed amount and returns everything that is left. The share of every vote is given by the vote's weight (which is computed during voting) and the total vote weight stored in post.total_vote_weight.

var pay_curators = function(post, max_reward_tokens) {
  let unclaimed = max_reward_tokens;
  const total_weight = post.total_vote_weight;
  post.active_votes.forEach(vote => {
    // use Math.max(0, ..) to filter downvotes
    unclaimed -=
      Math.floor(
        1000.0 * Math.max(0, max_reward_tokens * vote.weight / total_weight)
      ) / 1000.0;
  });

  return Math.max(0, unclaimed);
};

3. Reward all beneficiaries

A post may contain a list of beneficiares that receive a share of the author rewards, after the curators have been rewarded.

// re-add unclaimed curation tokens to the author tokens
  author_tokens += pay_curators(post, curation_tokens);

  let total_beneficiary = 0;
  // pay beneficiaries
  post.beneficiaries.forEach(b => {
    total_beneficiary +=
      Math.floor(1000.0 * author_tokens * b.weight / 10000.0) / 1000.0;
  });

  author_tokens -= total_beneficiary;

4. Reward the author

Finally, the author of the post is rewarded with everything that is left after the curators and beneficiaries have been payed. First the author_tokens are divided into a part to be payed in hive power (to_hive) and a part to be payed in HBD (to_hbd) according to post.percent_hive_dollars. Be careful here, the HBD share maxes out at 50%, such that percent_hive_dollars has to be divided by 2*10000.0. As mentioned above, if the market cap of HBD is too high, not the full amount of to_hbd is credited in HBD but only the share given by hbd_print_rate (of the dynamic global properties object). The remaining amount is payed in hive. createPayout returns an array with the payout shares in hive, HBD, and hive power.

var createPayout = function(
  author_tokens,
  percent_hive_dollars,
  hbd_print_rate,
  current_hive_price
) {
  const hbd_hive = author_tokens * percent_hive_dollars / (2 * 10000.0);
  // if hbd_print_rate is below 10000, part of the payout is in hive instead of hBD
  const to_hive = hbd_hive * (10000 - hbd_print_rate) / 10000.0;
  const to_hbd = hbd_hive - to_hive;
  const vesting_hive = author_tokens - hbd_hive;

  return [
    Math.floor(1000.0 * to_hive) / 1000.0,
    Math.floor(1000.0 * to_hbd * current_hive_price) / 1000.0,
    Math.floor(1000.0 * vesting_hive) / 1000.0
  ];
};

Putting it all together

Using the functions and code snippets explained above, the full payout function can be put together as below:

var payout = function(post, reward_fund, median_price_history, dgp) {
  const recent_claims = parseFloat(reward_fund.recent_claims);
  const reward_balance = parseFloat(
    reward_fund.reward_balance.replace(" hive", "")
  );
  const current_hive_price = median_price_history.base.replace(" HBD", "");

  const reward = get_rshare_reward(
    post,
    recent_claims,
    reward_balance,
    current_hive_price
  );

  if (reward == 0) return [0, 0, 0];

  const curation_tokens =
    Math.floor(
      1000.0 * reward * reward_fund.percent_curation_rewards / 10000.0
    ) / 1000.0;
  let author_tokens = reward - curation_tokens;

  // re-add unclaimed curation tokens to the author tokens
  author_tokens += pay_curators(post, curation_tokens);

  let total_beneficiary = 0;
  // pay beneficiaries
  post.beneficiaries.forEach(b => {
    total_beneficiary +=
      Math.floor(1000.0 * author_tokens * b.weight / 10000.0) / 1000.0;
  });

  author_tokens -= total_beneficiary;

  return createPayout(
    author_tokens,
    post.percent_hive_dollars,
    dgp.hbd_print_rate,
    current_hive_price
  );
};

So far the explanation of writing the code + writing the code. Hope I helped :)

thanks to @nafest on the writing the original code.