Posts

Estimating Leofinance Upcoming Curation

0 views
·
8 min read

Pending SCOT curation is probably something a lot of people have wanted for quite a while now. I certainly have. I really thought someone else would have done it by now but surprisingly not, and while procrastinating on studying for my Data Structures and Algorithms test, I decided to write some bad code and complete something I've wanted to do for a while.

The first step was to get the formula for leo's vote from rshares. Sadly nobody I asked knew that so I ended up looking at the nitrous code. From there, I found the following lines of code at https://github.com/hive-engine/nitrous/blob/ecd4fdc2e33d93eef1a01ba684e52b1a61c4564c/src/app/components/elements/Voting.jsx#l336.

const newValue = applyRewardsCurve(rsharesTotal + rshares); 
valueEst = (newValue / scotDenom - scot_pending_token).toFixed(scotPrecision); 

newValue was the amount of rShares after applying the curation curve. For our case, since we just need this for LEO, we can simplify the applyRewardsCurve function to

function applyRewardsCurve(rshares){ 
  return rshares * scotInfo.reward_pool / scotInfo.pending_rshares 
} 

from the one that's on nitrous. Basically, since Leo's author constant is 1, we can take part of the math out. Now we need to know what scotInfo is. Those values can be found at https://scot-api.hive-engine.com/info?token=LEO. So we just use something to fetch them for us.

async function loadScotValues(){ 
  let res = await axios("https://scot-api.hive-engine.com/info?token=LEO") 
  return res.data 
} 

Now that we have that, we can put it all together to approximate the leo vote value on the particular post.

function calculateLeoValue(rshares){ 
  return applyRewardsCurve(rshares) / SCOT_DENOM 
} 

SCOT_DENOM in this case is 1000, since it's just 10^(TokenPrecision) and leo's precision is 3.

This was by far the hardest part for me as I hadn't worked with this before. From there on we got to reuse a lot of code from older projects. From FRIDAY I copied over the account history script that I had to pull the account history of a user with filters.

async function getAccountHistory(account, start = -1, init = false, filter, endDays) { 
  if (start < 0 && !init) { 
      return []; 
  } 
  let res = null; 
  try { 
      res = await axios.post("https://api.deathwing.me", { 
          "id": 0, 
          "jsonrpc": '2.0', 
          "method": 'call', 
          "params": ['database_api', 'get_account_history', [account, start, (start < 0) ? 1000 : Math.min(start, 1000), ...filter]] 
      }); 
  } catch (e) { 
      return await getAccountHistory(account, start, false, filter, endDays); 
  } 
  if (res.data.error) { 
      if (res.data.error.code === -32003) { 
          const newStart = res.data.error.message.match('(?<=start=)(.*)(?=.)')[0]; 
          return await getAccountHistory(account, newStart, false, filter, endDays); 
      } 
      if (res.data.error.code === -32603) { 
          return await getAccountHistory(account, start, false, filter, endDays); 
      } 
  } 
  let result = res.data.result; 
  let con = true; 
  const valid = []; 
  let newStart = -1; 
  result = result.reverse(); 
  for (const i in result) { 
      const opId = result[i][0]; 
      const opAction = result[i][1]; 
      const actionTime = moment.utc(opAction.timestamp).unix(); 
      if (moment.utc().subtract(endDays, 'day').unix() > actionTime) { 
          con = false; 
          break; 
      } 
      newStart = opId - 1; 
      valid.push(opAction); 
  } 
  if (con) { 
      return valid.concat(await getAccountHistory(account, newStart, false, filter, endDays)); 
  } 
  return valid; 
} 

This code has proven reliable so far and so I have no problem continually using it. It's recursive, looks pretty and does its job very well. After that we need something to get all votes cast by the user.

async function getUserVoteHistory(username){ 
  let votes = await getAccountHistory(username, -1, true, [ '1', null ], 7) 
  let cleanVotes = [] 
  for (let i in votes){ 
    let op = votes[i].op[1] 
    cleanVotes.push({author : op.author, permlink : op.permlink}) 
  } 
  return cleanVotes 
} 

This just uses account history to do most of the heavy work, looking for votes cast in the last 7 days by the user defined. We throw all of that into an array for later use. Our start point is -1 so that way we grab the latest data. The [ '1', null ] is the filters that we use, this one is specified for just votes. The filters make life easy as we just get back the data that we want, no other useless information(here we just care about votes, there's no use of any other type of transaction on hive for us).

Up next is getting information about a post from the scot api.

async function getPostLeoVoterInfo(author, permlink, voter){ 
  let res = await axios(`https://scot-api.hive-engine.com/@${author}/${permlink}?hive=1`) 
  let data = res.data 
  if (!data.LEO || !data.LEO.active_votes){ 
    return false 
  } 
  if (data.LEO.last_payout !== "1970-01-01T00:00:00") { 
    return false 
  } 
  let activeVotes = data.LEO.active_votes 
  for (let i in activeVotes){ 
    if (activeVotes[i].voter === voter){ 
      return {voteInfo: activeVotes[i], postCreated: moment.utc(data.LEO.created)} 
    } 
  } 
  return false 
} 

This is definitely the slowest part, as the scot api isn't the fastest, and we'll be needing to call this on a whole bunch of posts. We check to see if the post has already paid out, and if it has we return false to ignore it. From each post, we need 2 pieces of info, when it was created and the our user's vote stats, if it is there. If that part doesn't exist we just return false so we can let our final calculator know to ignore it. This would help for stuff like posts that aren't on leo.

Finally we have the big bad script that does combines everything we've done so far and puts them together to calculate the total curation.

async function calculateLeoPendingCurationRewards(user){ 
  scotInfo = await loadScotValues() 
  let postsVoted = await getUserVoteHistory(user) 
  let voteInfo = [] 
  let totalPendingCuration = 0 
  let c = 1 
  for (let i in postsVoted){ 
    console.log("On Post " + c++ + " of total posts " + postsVoted.length + ".") 
    let postInfo = await getPostLeoVoterInfo(postsVoted[i].author, postsVoted[i].permlink, user) 
    if(postInfo){ 
      let postVoteData = postInfo.voteInfo 
      let voteTimeSeconds = moment.utc(postVoteData.timestamp).diff(postInfo.postCreated, "seconds") 
      let multiplier = moment.utc(postVoteData.timestamp).diff(postInfo.postCreated, "seconds") >= 300 ? 1 : voteTimeSeconds / 300 
      let pendingCuration = (calculateLeoValue(postVoteData.rshares) / 2) * (multiplier) 
      if (pendingCuration > 0){ 
        totalPendingCuration += pendingCuration 
      } 
      postsVoted[i].weight = postVoteData.weight 
      postsVoted[i].rshares = postVoteData.rshares 
      postsVoted[i].pendingCuration = pendingCuration > 0 ? pendingCuration : 0 
      voteInfo.push(postsVoted) 
    } 
  } 
  console.log(voteInfo, "Total Pending Curation: " + totalPendingCuration.toFixed(3)) 
} 

We start by loading in the scot values as thats pretty vital for our calculations. Then we get all the posts that the user has voted. By using the helper functions that we made earlier, we have a much easier time reading on whats going on. We create voteInfo to store info on all the votes that we cast and totalPendingCuration to keep a sum of all our curation rewards so far. c is just there to count how many posts we have done so we can log it and ensure that its going on, as getting each post from the scot api takes quite some time.

We then go through every single vote that our user had cast, getting the post info of it. Remember when we returned false from getPostLeoVoterInfo? We use that here to know when to skip a certain post if theres no data for it. After that we calculate the second difference between the when the vote was cast and when the post was created. If you cast your vote before the 5 minute mark on LEO, you lose a part of your curation, and since that's linear we can account for that. 5 minutes in seconds is 300 seconds, so if your vote time difference was bigger than 300, we give you a multiplier of 1(full weight) if it was anything less, your multiplier is the time difference divided by 300.

We then calculate the pending curation on that particular post. We use the calculateLeoValue function we created earlier to do that for us. We then need to divide that by 2 because half the reward goes to the author. Then we multiply by the multiplier in case you voted early and gave up part of your curation.

Then we just clean stuff up a bit. If your curation was below 0 it just means that you downvoted the post, otherwise it was an upvote and you are getting rewarded for it so we add it to your totalPendingCuration. Finally we add a bit more information about the vote to the current element of postsVoted so we can use that to do further analysis in the future. Finally we log the total pending curation. It does take some time to run so you'll need to be patient.

I ran it on @meowcurator and it got me a value of 133.742 which seems about accurate for the account. If you want me to run it for you, just let me know and I can do so. I can probably make a UI for you to run whenever you want in the future, but that'll be after this week is over if I do it at all. Hopefully someone else uses this information to do so.

This does have a problem with downvotes. If the post you voted gets downvoted, it won't get you the right amount of that post. I believe I know how I can fix it, and if I do I'll edit the post to account for that, but for now this should be accurate enough for most people.

Here's the entire code if someone wants to use it.

let axios = require("axios") 
let moment = require("moment") 
 
const SCOT_DENOM = 1000 
let scotInfo = {} 
 
async function loadScotValues(){ 
  let res = await axios("https://scot-api.hive-engine.com/info?token=LEO") 
  return res.data 
} 
 
function applyRewardsCurve(rshares){ 
  return rshares * scotInfo.reward_pool / scotInfo.pending_rshares 
} 
 
function calculateLeoValue(rshares){ 
  return applyRewardsCurve(rshares) / SCOT_DENOM 
} 
 
async function getAccountHistory(account, start = -1, init = false, filter, endDays) { 
  if (start < 0 && !init) { 
      return []; 
  } 
  let res = null; 
  try { 
      res = await axios.post("https://api.deathwing.me", { 
          "id": 0, 
          "jsonrpc": '2.0', 
          "method": 'call', 
          "params": ['database_api', 'get_account_history', [account, start, (start < 0) ? 1000 : Math.min(start, 1000), ...filter]] 
      }); 
  } catch (e) { 
      return await getAccountHistory(account, start, false, filter, endDays); 
  } 
  if (res.data.error) { 
      if (res.data.error.code === -32003) { 
          const newStart = res.data.error.message.match('(?<=start=)(.*)(?=.)')[0]; 
          return await getAccountHistory(account, newStart, false, filter, endDays); 
      } 
      if (res.data.error.code === -32603) { 
          return await getAccountHistory(account, start, false, filter, endDays); 
      } 
  } 
  let result = res.data.result; 
  let con = true; 
  const valid = []; 
  let newStart = -1; 
  result = result.reverse(); 
  for (const i in result) { 
      const opId = result[i][0]; 
      const opAction = result[i][1]; 
      const actionTime = moment.utc(opAction.timestamp).unix(); 
      if (moment.utc().subtract(endDays, 'day').unix() > actionTime) { 
          con = false; 
          break; 
      } 
      newStart = opId - 1; 
      valid.push(opAction); 
  } 
  if (con) { 
      return valid.concat(await getAccountHistory(account, newStart, false, filter, endDays)); 
  } 
  return valid; 
} 
 
async function getUserVoteHistory(username){ 
  let votes = await getAccountHistory(username, -1, true, [ '1', null ], 7) 
  let cleanVotes = [] 
  for (let i in votes){ 
    let op = votes[i].op[1] 
    cleanVotes.push({author : op.author, permlink : op.permlink}) 
  } 
  return cleanVotes 
} 
 
async function getPostLeoVoterInfo(author, permlink, voter){ 
  let res = await axios(`https://scot-api.hive-engine.com/@${author}/${permlink}?hive=1`) 
  let data = res.data 
  if (!data.LEO || !data.LEO.active_votes){ 
    return false 
  } 
  if (data.LEO.last_payout !== "1970-01-01T00:00:00") { 
    return false 
  } 
  let activeVotes = data.LEO.active_votes 
  for (let i in activeVotes){ 
    if (activeVotes[i].voter === voter){ 
      return {voteInfo: activeVotes[i], postCreated: moment.utc(data.LEO.created)} 
    } 
  } 
  return false 
} 
 
async function calculateLeoPendingCurationRewards(user){ 
  scotInfo = await loadScotValues() 
  let postsVoted = await getUserVoteHistory(user) 
  let voteInfo = [] 
  let totalPendingCuration = 0 
  let c = 1 
  for (let i in postsVoted){ 
    console.log("On Post " + c++ + " of total posts " + postsVoted.length + ".") 
    let postInfo = await getPostLeoVoterInfo(postsVoted[i].author, postsVoted[i].permlink, user) 
    if(postInfo){ 
      let postVoteData = postInfo.voteInfo 
      let voteTimeSeconds = moment.utc(postVoteData.timestamp).diff(postInfo.postCreated, "seconds") 
      let multiplier = moment.utc(postVoteData.timestamp).diff(postInfo.postCreated, "seconds") >= 300 ? 1 : voteTimeSeconds / 300 
      let pendingCuration = (calculateLeoValue(postVoteData.rshares) / 2) * (multiplier) 
      if (pendingCuration > 0){ 
        totalPendingCuration += pendingCuration 
      } 
      postsVoted[i].weight = postVoteData.weight 
      postsVoted[i].rshares = postVoteData.rshares 
      postsVoted[i].pendingCuration = pendingCuration > 0 ? pendingCuration : 0 
      voteInfo.push(postsVoted) 
    } 
  } 
  console.log(voteInfo, totalPendingCuration) 
} 
 
calculateLeoPendingCurationRewards("meowcurator") //Chang the username to the person who's curation information you want. 

The only dependencies are axios for fetching data and moment for better date stuff(yes I know its deprecated, but its still works well for something small like this).