Estimate author payouts with Steem.js

in utopian-io •  7 years ago  (edited)

What Will I Learn?

In this tutorial,

  • You will learn, how a payout for a post on the STEEM 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 STEEM 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 STEEM blockchain are computed and how to rebuild this computation with Steem.js. Based on the STEEM source code, I will try to rebuild the payout process as close as possible.
With this tutorial, you will be able to predict the author payouts in STEEM, SBD, and STEEM power for a post more accurately than the popup on steemit, which does not distinguish between author/curation/beneficiary rewards.

Bildschirmfoto 2018-03-23 um 17.31.16.png

Setup a Node.js project with Steem.js

I will not go into detail here, just 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.

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

In order to compute the author rewards for a post, we need to retrieve four objects from the STEEM blockchain with Steem.js. The post (of course), the reward balance, the dynamic global properties, and the current STEEM 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: sbd_payout_rate. When a new post is created, one must specify, how the payout should be split in SBD and SP. Typically a 50%/50% split is selected. However, of these 50% only the percentage determined by sbd_payout_rate is payed in SBD, while the remaining share is payed in STEEM. The sbd_payout_rate is a mechanism to keep the market cap of SBD at a reasonable relation to the market cap of STEEM. If this relation rises above 2%, sbd_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 SBD and STEEM, we need the current median history price, which is the median of the STEEM price over the last three and a half days. As the median price fluctuates, the author payout in SBD computed by this tutorial fluctuates as well (the share which is credited as STEEM 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 is found in reward.cpp. 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 STEEM 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 STEEM 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_claims (btw., this claim is added to recent_claims in the current STEEM block). STEEM uses fixed point arithmetics everywhere. Amounts in STEEM 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 STEEM blockchain.

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

Posts with a total reward of less than 0.020 SBD 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 steem_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 SBD so it must be divided by the steem_price to match the unit of reward).

var get_rshare_reward = function(
  post,
  recent_claims,
  reward_balance,
  steem_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 SBD.
  if (reward * steem_price < 0.02) return 0;

  const max_steem =
    parseFloat(post.max_accepted_payout.replace(" SBD", "")) / steem_price;

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

2. Reward all curators

The overall logic for the payout of a post can be found in the function database::cashout_comment_helper() in database.cpp. After the total reward has been computed, all votes on the post receive their curation reward with the helper function pay_curators(). 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 25%), 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 STEEM allocated in curation_tokens may be spent as curation reward, since votes within the first 30 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. E.g., if you post on utopian.io, the account utopian.pay is added as beneficiary with a weight of 25%.

  // 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 STEEM power (to_steem) and a part to be payed in SBD (to_sbd) according to post.percent_steem_dollars. Be careful here, the SBD share maxes out at 50%, such that percent_steem_dollars has to be divided by 2*10000.0. As mentioned above, if the market cap of SBD is too high, not the full amount of to_sbd is credited in SBD but only the share given by sbd_print_rate (of the dynamic global properties object). The remaining amount is payed in STEEM. createPayout returns an array with the payout shares in STEEM, SBD, and SP.

var createPayout = function(
  author_tokens,
  percent_steem_dollars,
  sbd_print_rate,
  current_steem_price
) {
  const sbd_steem = author_tokens * percent_steem_dollars / (2 * 10000.0);
  // if sbd_print_rate is below 10000, part of the payout is in STEEM instead of SBD
  const to_steem = sbd_steem * (10000 - sbd_print_rate) / 10000.0;
  const to_sbd = sbd_steem - to_steem;
  const vesting_steem = author_tokens - sbd_steem;

  return [
    Math.floor(1000.0 * to_steem) / 1000.0,
    Math.floor(1000.0 * to_sbd * current_steem_price) / 1000.0,
    Math.floor(1000.0 * vesting_steem) / 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(" STEEM", "")
  );
  const current_steem_price = median_price_history.base.replace(" SBD", "");

  const reward = get_rshare_reward(
    post,
    recent_claims,
    reward_balance,
    current_steem_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_steem_dollars,
    dgp.sbd_print_rate,
    current_steem_price
  );
};

To call payout(), you must first get a post object and the global objects that have to be passed to payout() with

const reward_fund = await getRewardFund();
const median_price_history = await getCurrentMedianHistoryPrice();
const dgp = await getDynamicGlobalProperties();
const post = await getContent(
  "abh12345",  // author
  "how-much-available-sp-is-there-for-steemit-com-content" // permlink
);

console.log(payout(post, reward_fund, median_price_history, dgp));

You may change the author and permlink to the post of your interest. For the post above, I get the following output at the time of this writing.

$ node payout.js
[ 0, 36.345, 17.144 ]

The full source code of payout.js can be found here.



Posted on Utopian.io - Rewarding Open Source Contributors

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Thank you for the contribution. It has been approved.

You can contact us on Discord.
[utopian-moderator]

Thank you for reviewing.

Hey @miguepersa, I just gave you a tip for your hard work on moderation. Upvote this comment to support the utopian moderators and increase your future rewards!

Loading...

Hey @nafestw I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x

Realy helpful my friend thank you