import steem from 'steem'
import sc2 from 'sc2-sdk'
import base58 from 'bs58'
import getSlug from 'speakingurl'
import secureRandom from 'secure-random'
// babel-polyfill required for the async/wait
import 'babel-polyfill' // eslint-disable-line no-unused-vars
module.exports.EasySteem = class EasySteem {
/**
* initialize easysteem
* @constructor
* @param {String} appId app identifier on your steemconnect dashboard (https://steemconnect.com/dashboard)
* @param {String} appName app name that will be part of the metadata posted within your app
* @param {String} appVersion app version that will be part of the metadata posted within your app
*/
constructor (appId, appName, appVersion) {
this.steemconnectapi = sc2.Initialize({
'app': appId
})
this.appName = appName
this.appVersion = appVersion
this.steem = steem
}
/**
* reward options available on Steem: CENT_PERCENT_SP, FIFTY_PERCENT_SP_SBD or NONE
*/
static get REWARD_OPTIONS () {
return {
'CENT_PERCENT_SP': '100',
'FIFTY_PERCENT_SP_SBD': '50',
'NONE': '0'
}
}
/**
* order options: REPUTATION, PAYOUT, PERCENT, OLDEST, NEWEST
*/
static get ORDER_OPTIONS () {
return {
'REPUTATION': 'REPUTATION',
'PAYOUT': 'PAYOUT',
'PERCENT': 'PERCENT',
'OLDEST': 'OLDEST',
'NEWEST': 'NEWEST'
}
}
/**
* get the url where the user can log into steemconnect
* @param {Array<String>} scope scope of your application (https://github.com/steemit/steemconnect/wiki/OAuth-2#scopes)
* @param {String} callbackUrl url where the users will be redirected after interacting with steemconnect
* @param {String} state data that will be passed to the callbackURL after the user has logged in
* @returns {String} url to log the user in
*/
getLoginUrl (scope, callbackUrl, state = null) {
this.steemconnectapi.setScope(scope)
this.steemconnectapi.setCallbackURL(callbackUrl)
return this.steemconnectapi.getLoginURL(state)
}
/**
* log the current user out
* @returns {Promise<JSON>} return the transaction details
*/
logout () {
return this.steemconnectapi.revokeToken()
}
/**
* get the profile of the current user
* @returns {Promise<JSON>} return the user profile
*/
me () {
return this.steemconnectapi.me()
}
/**
* parse a url returned by Steemconnect to get the account, the access token and the expiration time
* @param {String} url url to parse
* @example
* easysteem.parseReturnedUrl('https://my-awesome-website.com/steemconnect/?access_token=THISISASECUREDTOKEN&expires_in=604800&username=harpagon)
* @returns {JSON} account, accessToken and expTime
*/
parseReturnedUrl (url) {
let urlSplitted = url.split('?')[1].split('&')
let accessToken = urlSplitted.filter((el) => {
if (el.match('access_token') !== null) {
return true
}
})[0].split('=')[1]
let expTime = urlSplitted.filter((el) => {
if (el.match('expires_in') !== null) {
return true
}
})[0].split('=')[1]
let account = urlSplitted.filter((el) => {
if (el.match('username') !== null) {
return true
}
})[0].split('=')[1]
this.setAccessToken(accessToken)
this.setAccount(account)
return {
account,
accessToken,
expTime
}
}
/**
* set the steem account that will be used to perform the actions
* @param {String} account steem account
*/
setAccount (account) {
this.account = account
}
/**
* set the steemconnect oAuth2 access token
* @param {String} accessToken steemconnect oAuth2 access token
*/
setAccessToken (accessToken) {
this.steemconnectapi.setAccessToken(accessToken)
}
/**
* update the current user metadata
* @param {JSON} metadata
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope custom_json
*/
updateUserMetadata (metadata) {
return this.steemconnectapi.updateUserMetadata(metadata)
}
/**
* upvote a post
* @param {String} postAuthor the username of the author of the post
* @param {String} postPermlink the permlink of the post
* @param {Number} weigth the weight of the vote (ex: 0, 1.24, 49.9, 100)
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope vote
* @example
* easysteem.upvote('harpagon', 'test-permlink', 49.99)
* .then(result => console.log(result))
* .catch(error => console.error(error.error, error.error_description))
*/
upvote (postAuthor, postPermlink, weigth) {
return this.steemconnectapi.vote(this.account, postAuthor, postPermlink, weigth * 100)
}
/**
* downvote a post
* @param {String} postAuthor the username of the author of the post
* @param {String} postPermlink the permlink of the post
* @param {Number} weigth the weight of the vote (ex: 0, 1.24, 49.9, 100)
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope vote
* @example
* easysteem.downvote('harpagon', 'test-permlink', 49.99)
* .then(result => console.log(result))
* .catch(error => console.error(error.error, error.error_description))
*/
downvote (postAuthor, postPermlink, weigth) {
return this.upvote(this.account, postAuthor, postPermlink, weigth * -100)
}
/**
* post a new article
* @param {String} title title of the post
* @param {String} body markdown formatted body
* @param {String} category category of the post
* @param {Array<String>} tags tags of the post
* @param {String} rewardOption default is REWARD_OPTIONS.FIFTY_PERCENT_SP_SBD (REWARD_OPTIONS.CENT_PERCENT_SP, REWARD_OPTIONS.FIFTY_PERCENT_SP_SBD or REWARD_OPTIONS.NONE
* @param {Array<JSON>} beneficiaries array containing the beneficiaries and the weight for their reward ex: [{ 'account': 'harpagon', 'weight': 100 }]
* @param {JSON} jsonMetadata metadata attached to this post
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope comment, comment_options
* @example
* easysteem.createPost(
* 'title',
* 'body',
* 'category',
* ['tag1', 'tag2'],
* EasySteem.REWARD_OPTIONS.CENT_PERCENT_SP, // default is set to REWARD_OPTIONS.FIFTY_PERCENT_SP_SBD
* [
* {
* 'account': 'harpagon',
* 'weight': 50.99 // represents the percentage (50.99% in this case)
* },
* {
* 'account': 'account',
* 'weight': 0.01 // represents the percentage (0.01% in this case)
* }
* ],
* {
* 'format': 'html' // default is set to mardown but you can override
* })
* .then(result => console.log(result))
* .catch(error => console.error(error.error, error.error_description))
*/
createPost (title, body, category, tags = [], rewardOption = EasySteem.REWARD_OPTIONS.FIFTY_PERCENT_SP_SBD, beneficiaries = [], jsonMetadata = {}) {
return new Promise(async (resolve, reject) => {
try {
tags.unshift(category)
const permlink = await this.createPermlink(title)
const result = await this.postOperation('', category, permlink, title, body, tags, rewardOption, beneficiaries, jsonMetadata)
resolve(result)
} catch (error) {
reject(error)
}
})
}
/**
* update an existing article
* @param {String} permlink permlink of the post to update
* @param {String} title title of the post
* @param {String} body markdown formatted body
* @param {Array<String>} tags tags of the post
* @param {JSON} jsonMetadata metadata attached to this post
* @param {String} category will be used to update the post, if not provided, it will be retrieved via the Steem API (however the category can't be updated)
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope comment and comment_options
* @example
* easysteem.updatePost(
* 'permlink',
* 'title',
* 'body',
* ['tag1', 'tag2'],
* {
* 'format': 'html' // default is set to mardown but you can override
* },
* 'category')
* .then(result => console.log(result))
* .catch(error => console.error(error.error, error.error_description))
*/
updatePost (permlink, title, body, tags, jsonMetadata, category = null) {
if (category) {
return this.postOperation('', category, permlink, title, body, tags, null, [], jsonMetadata)
} else {
return new Promise(async (resolve, reject) => {
try {
const content = await this.getContent(this.account, permlink)
if (content.parent_permlink) {
const result = await this.postOperation('', content.parent_permlink, permlink, title, body, tags, null, [], jsonMetadata)
resolve(result)
} else {
reject(content)
}
} catch (error) {
reject(error)
}
})
}
}
/**
* comment a post or a comment
* @param {String} parentAuthor author of the post or comment to comment
* @param {String} parentPermlink permlink of the post or comment to comment
* @param {String} body markdown formatted body
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope comment and comment_options
* @example
* easysteem.createComment('harpagon', 'test-title', '**test comment**')
* .then(result => console.log(result))
* .catch(error => console.error(error.error, error.error_description))
*/
createComment (parentAuthor, parentPermlink, body) {
return new Promise(async (resolve, reject) => {
try {
const permlink = await this.createPermlink('', parentAuthor, parentPermlink)
const result = await this.postOperation(parentAuthor, parentPermlink, permlink, '', body, [], EasySteem.REWARD_OPTIONS.FIFTY_PERCENT_SP_SBD)
resolve(result)
} catch (error) {
reject(error)
}
})
}
/**
* update a comment
* @param {String} permlink permlink of the post or comment to comment
* @param {String} body markdiwn formatted body
* @param {String} parentAuthor parent author of the post or comment to comment (if not provided, this will be retrieved via the Steem API)
* @param {String} parentPermlink parent permlink of the post or comment to comment (if not provided, this will be retrieved via the Steem API)
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope comment and comment_options
* @example
* easysteem.updateComment('re-harpagon-test-title-20180312t034345437z', '**test update comment 3**')
* .then(result => console.log(result))
* .catch(error => console.error(error.error, error.error_description))
*/
updateComment (permlink, body, parentAuthor = null, parentPermlink = null) {
if (parentAuthor && parentPermlink) {
return this.postOperation(parentAuthor, parentPermlink, permlink, '', body)
} else {
return new Promise(async (resolve, reject) => {
try {
const content = await this.getContent(this.account, permlink)
if (content.parent_permlink) {
const result = await this.postOperation(content.parent_author, content.parent_permlink, permlink, '', body)
resolve(result)
} else {
reject(content)
}
} catch (error) {
reject(error)
}
})
}
}
/**
* delete a post or a comment
* @param {String} permlink permlink of the post or comment to delete
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope delete_comment
*/
deletePostOrComment (permlink) {
const operations = []
const commentOp = [
'delete_comment',
{
'author': this.account,
'permlink': permlink
}
]
operations.push(commentOp)
return this.steemconnectapi.broadcast(operations)
}
/**
* post on the Steem blockchain
* @param {String} parentAuthor parent author of the post or comment
* @param {String} parentPermlink parent permlink of the post or comment
* @param {String} title title of the post (empty for a comment)
* @param {String} body markdown formatted body
* @param {Array<String>} tags tags of the post
* @param {String} rewardOption REWARD_OPTIONS.CENT_PERCENT_SP, REWARD_OPTIONS.FIFTY_PERCENT_SP_SBD or REWARD_OPTIONS.NONE
* @param {Array<JSON>} beneficiaries array containing the beneficiaries and the weight for their reward
* @param {JSON} jsonMetadata metadata attached to this post
* @returns {Promise<JSON>} return the transaction details
*/
postOperation (parentAuthor, parentPermlink, permlink, title, body, tags = [], rewardOption = null, beneficiaries = [], jsonMetadata = {}) {
const operations = []
jsonMetadata = Object.assign(
{},
{
'tags': tags,
'app': `${this.appName}/${this.appVersion}`,
'format': 'markdown'
},
jsonMetadata)
const commentOp = [
'comment',
{
'parent_author': parentAuthor,
'parent_permlink': parentPermlink,
'author': this.account,
'permlink': permlink,
'title': title,
'body': body,
'json_metadata': JSON.stringify(jsonMetadata)
}
]
operations.push(commentOp)
const commentOptionsConfig = {
'author': this.account,
'permlink': permlink,
'allow_votes': true,
'allow_curation_rewards': true,
'max_accepted_payout': '1000000.000 SBD',
'percent_steem_dollars': 10000
}
if (rewardOption && (rewardOption === EasySteem.REWARD_OPTIONS.NONE)) {
commentOptionsConfig.max_accepted_payout = '0.000 SBD'
} else if (rewardOption && (rewardOption === EasySteem.CENT_PERCENT_SP)) {
commentOptionsConfig.percent_steem_dollars = 0
}
if (beneficiaries.length > 0) {
beneficiaries.sort(function (a, b) {
let textA = a.account.toUpperCase()
a.weight *= 100.00
let textB = b.account.toUpperCase()
b.weight *= 100.00
return (textA < textB) ? -1 : (textA > textB) ? 1 : 0
})
commentOptionsConfig.extensions = [
[
0,
{
// [{ 'account': 'harpagon', 'weight': 1000 }]
'beneficiaries': beneficiaries
}
]
]
}
if (rewardOption === EasySteem.REWARD_OPTIONS.NONE || rewardOption === EasySteem.REWARD_OPTIONS.CENT_PERCENT_SP || beneficiaries.length > 0) {
operations.push(['comment_options', commentOptionsConfig])
}
return this.steemconnectapi.broadcast(operations)
}
/**
* reblog a post
* @param {String} author the author of the post to reblog
* @param {String} permlink the permlink of the post to reblog
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope custom_json
*/
reblog (author, permlink) {
return this.steemconnectapi.reblog(this.account, author, permlink)
}
/**
* follow an author
* @param {String} author author to follow
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope custom_json
*/
follow (author) {
return this.steemconnectapi.follow(this.account, author)
}
/**
* unfollow an author
* @param {String} author author to unfollow
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope custom_json
*/
unfollow (author) {
return this.steemconnectapi.unfollow(this.account, author)
}
/**
* ignore an author
* @param {String} author author to ignore
* @returns {Promise<JSON>} return the transaction details
* @steemconnect_scope custom_json
* @
*/
ignore (author) {
return this.steemconnectapi.ignore(this.account, author)
}
/**
* get the content of a post or comment
* @param {String} account the account that owns the post or comment
* @param {String} permlink the permlink of the post or comment
* @returns {Promise<JSON>} return the content of the post or comment
*/
getContent (account, permlink) {
return steem.api.getContentAsync(account, permlink)
}
/**
* retrieve a user account
* @param {String} username username for which you want to retrieve the account details
* @returns {JSON} user account details
*/
getUserAccount (username) {
return this.getUserAccounts([username])
}
/**
* retrieve a user account
* @param {Array<String>} usernames usernames for which you want to retrieve the account details
* @returns {JSON} user account details
*/
getUserAccounts (usernames) {
return this.steem.api.getAccountsAsync(usernames)
}
/**
* get the followers of a user
* @param {String} username the username for which you want to retrieve the followers
* @returns {Array<JSON>} an array containing the followers
*/
getFollowers (username) {
return new Promise(async resolve => {
let retVal = []
let startFollower = ''
const followCount = await this.steem.api.getFollowCountAsync(username)
const count = followCount.follower_count
for (let i = 0; i < count; i += 1000) {
let temp = await this.steem.api.getFollowersAsync(username, startFollower, 'blog', 1000)
Array.prototype.push.apply(retVal, temp)
startFollower = retVal[retVal.length - 1].follower
}
resolve(retVal)
})
}
/**
* get the following of a user
* @param {String} username the username for which you want to retrieve the following
* @returns {Array<JSON>} an array containing the following
*/
getFollowing (username) {
return new Promise(async resolve => {
let retVal = []
let startFollower = ''
const followCount = await this.steem.api.getFollowCountAsync(username)
const count = followCount.following_count
for (let i = 0; i < count; i += 1000) {
let temp = await this.steem.api.getFollowingAsync(username, startFollower, 'blog', 100)
Array.prototype.push.apply(retVal, temp)
startFollower = retVal[retVal.length - 1].follower
}
resolve(retVal)
})
}
/**
* get the active votes of a post or comment
* @param {String} author author of the post or comment
* @param {String} permlink permlink of the post or comment
* @param {String} orderBy default EasySteem.ORDER_OPTIONS.PAYOUT but REPUTATION, PERCENT, PAYOUT available
* @returns {Array<JSON>} return the active votes of the post or comment ordered
*/
getActiveVotes (author, permlink, orderBy = EasySteem.ORDER_OPTIONS.PAYOUT) {
return new Promise(async resolve => {
let votes = await this.steem.api.getActiveVotesAsync(author, permlink)
await this.orderVotes(votes, orderBy)
resolve(votes)
})
}
/**
* get the replies to a post or comment
* @param {String} author author of the post or comment
* @param {String} permlink permlink of the post or comment
* @param {String} orderBy default EasySteem.ORDER_OPTIONS.PAYOUT but OLDEST, NEWEST, REPUTATION, PAYOUT available
* @returns {Array<JSON>} return the comments of the post or comment ordered
*/
getContentReplies (author, permlink, orderBy = EasySteem.ORDER_OPTIONS.PAYOUT) {
return new Promise(async resolve => {
let comments = await this.steem.api.getContentRepliesAsync(author, permlink)
await this.orderComments(comments, orderBy)
resolve(comments)
})
}
/**
* parse a payout amount from AMOUNT SDB to $AMOUNT
* @param {String} amount string representing the SBD amount
* @returns {String} parsed payout amount
* @link https://github.com/steemit/steemit.com/blob/47fd0e0846bd8c7c941ee4f95d5f971d3dc3981d/app/utils/ParsersAndFormatters.js
*/
parsePayoutAmount (amount) {
return parseFloat(String(amount).replace(/\s[A-Z]*$/, ''))
}
/**
* Calculates Payout Details
* @param {JSON} post post JSON
* @returns {JSON} JSON representing the payout details
* @link https://github.com/steemit/steemit.com/blob/47fd0e0846bd8c7c941ee4f95d5f971d3dc3981d/app/components/elements/Voting.jsx
*/
calculatePayout (post) {
const payoutDetails = {}
const activeVotes = post.active_votes
const parentAuthor = post.parent_author
const cashoutTime = post.cashout_time
const maxPayout = this.parsePayoutAmount(post.max_accepted_payout)
const pendingPayout = this.parsePayoutAmount(post.pending_payout_value)
const promoted = this.parsePayoutAmount(post.promoted)
const totalAuthorPayout = this.parsePayoutAmount(post.total_payout_value)
const totalCuratorPayout = this.parsePayoutAmount(post.curator_payout_value)
const isComment = parentAuthor !== ''
let payout = pendingPayout + totalAuthorPayout + totalCuratorPayout
if (payout < 0.0) payout = 0.0
if (payout > maxPayout) payout = maxPayout
payoutDetails.payoutLimitHit = payout >= maxPayout
// There is an 'active cashout' if: (a) there is a pending payout, OR (b)
// there is a valid cashout_time AND it's NOT a comment with 0 votes.
const cashoutActive =
pendingPayout > 0 ||
(cashoutTime.indexOf('1969') !== 0 && !(isComment && activeVotes.length === 0))
if (cashoutActive) {
payoutDetails.potentialPayout = pendingPayout
}
if (promoted > 0) {
payoutDetails.promotionCost = promoted
}
if (cashoutActive) {
// Append '.000Z' to make it ISO format (YYYY-MM-DDTHH:mm:ss.sssZ).
payoutDetails.cashoutInTime = cashoutTime + '.000Z'
}
if (maxPayout === 0) {
payoutDetails.isPayoutDeclined = true
} else if (maxPayout < 1000000) {
payoutDetails.maxAcceptedPayout = maxPayout
}
if (totalAuthorPayout > 0) {
payoutDetails.pastPayouts = totalAuthorPayout + totalCuratorPayout
payoutDetails.authorPayouts = totalAuthorPayout
payoutDetails.curatorPayouts = totalCuratorPayout
}
return payoutDetails
}
/**
* check and modifiy a permlink if necessary to fit the Steem blockchain requirements
* @param {String} permlink permlink to check
* @returns permlink checked and potentially modified
*/
checkPermLinkLength (permlink) {
if (permlink.length > 255) {
// STEEMIT_MAX_PERMLINK_LENGTH
permlink = permlink.substring(permlink.length - 255, permlink.length)
}
// only letters numbers and dashes shall survive
permlink = permlink.toLowerCase().replace(/[^a-z0-9-]+/g, '')
return permlink
}
slug (text) {
return getSlug(text.replace(/[<>]/g, ''), { truncate: 128 })
}
/**
* create a permlink
* @param {String} title title of the post (empty for a comment)
* @param {String} parentAuthor parent author of the comment
* @param {String} parentPermlink parent permlink of the comment
* @returns {String} permlink
* @link: https://github.com/steemit/steemit.com/blob/ded8ecfcc9caf2d73b6ef12dbd0191bd9dbf990b/app/redux/TransactionSaga.js
*/
createPermlink (title, parentAuthor = null, parentPermlink = null) {
let permlink
if (title && title.trim() !== '') {
let s = this.slug(title)
if (s === '') {
s = base58.encode(secureRandom.randomBuffer(4))
}
return this.steem.api.getContentAsync(this.account, s)
.then(content => {
let prefix
if (content.body !== '') {
// make sure slug is unique
prefix = `${base58.encode(secureRandom.randomBuffer(4))}-`
} else {
prefix = ''
}
permlink = prefix + s
return this.checkPermLinkLength(permlink)
})
.catch(err => {
console.warn('Error while getting content', err)
return permlink
})
}
// comments: re-parentauthor-parentpermlink-time
const timeStr = new Date().toISOString().replace(/[^a-zA-Z0-9]+/g, '')
parentPermlink = parentPermlink.replace(/(-\d{8}t\d{9}z)/g, '')
permlink = `re-${parentAuthor}-${parentPermlink}-${timeStr}`
return Promise.resolve(this.checkPermLinkLength(permlink))
}
/**
* Refresh the global propertis linked to Steem and the rates
*/
refreshSteemProperties () {
return Promise.all([
this.steem.api.getRewardFundAsync('post'),
this.steem.api.getDynamicGlobalPropertiesAsync(),
this.getCryptoCurrencyPrice('STEEM'),
this.getCryptoCurrencyPrice('SBD')
])
.then((results) => {
this.steemProperties = {}
// set the reward balance and the recent claims
this.steemProperties.rewardBalance = this.parsePayoutAmount(results[0].reward_balance)
this.steemProperties.recentClaims = this.parsePayoutAmount(results[0].recent_claims)
// set other data
this.steemProperties.totalVestingFund = this.parsePayoutAmount(results[1].total_vesting_fund_steem)
this.steemProperties.totalVestingShares = this.parsePayoutAmount(results[1].total_vesting_shares)
this.steemProperties.maxVirtualBandwidth = parseInt(results[1].max_virtual_bandwidth, 10)
// set the rates
this.steemProperties.steemRate = results[2]
this.steemProperties.sbdRate = results[3]
})
}
/**
* calculate the Steem value of a user vote
* @param {JSON} user a user object
* @param {Number} voteWeight the weight of the vote to calculate
* @param {Number} numberDecimals default 2, number of decimals to return
* @param {Boolean} refreshSteemProperties default true, refresh the Steem properties (Steem rate, etc...)
* @returns {Number} the value of the vote in Steem
*/
calculateVoteValue (user, voteWeight = 100.00, numberDecimals = 2, refreshSteemProperties = true) {
return new Promise(async resolve => {
if (!this.steemProperties || refreshSteemProperties) {
await this.refreshSteemProperties()
}
const votingPower = this.calculateVotingPower(user, numberDecimals) * 100
voteWeight = voteWeight * 100
const vestingShares = parseInt(this.calculateUserVestingShares(user) * 1e6, 10)
const power = votingPower * voteWeight / 10000 / 50
const rshares = power * vestingShares / 10000
resolve((rshares / this.steemProperties.recentClaims * this.steemProperties.rewardBalance * this.steemProperties.steemRate).toFixed(numberDecimals))
})
}
/**
* calculate a user's vesting shares
* @param {JSON} user a user object
* @returns {Number} the user's vesting shares
*/
calculateUserVestingShares (user) {
const vestingShares = parseFloat(this.parsePayoutAmount(user.vesting_shares))
const receivedVestingShares = parseFloat(this.parsePayoutAmount(user.received_vesting_shares))
const delegatedVestingShares = parseFloat(this.parsePayoutAmount(user.delegated_vesting_shares))
return vestingShares + receivedVestingShares - delegatedVestingShares
}
/**
* calculate the total amount of Steem Power delegated
* @param {JSON} user
* @param {Number} totalVestingShares
* @param {Number} totalVestingFundSteem
*/
calculateTotalDelegatedSP (user, totalVestingShares, totalVestingFundSteem) {
const receivedSP = parseFloat(
this.vestToSteem(user.received_vesting_shares, totalVestingShares, totalVestingFundSteem)
)
const delegatedSP = parseFloat(
this.vestToSteem(user.delegated_vesting_shares, totalVestingShares, totalVestingFundSteem)
)
return receivedSP - delegatedSP
}
/**
* calculate the voting power of a user
* @param {JSON} user a user object
* @param {Number} numberDecimals number of decimals to return, default 2
* @return {Number} a number representing the voting power of the user
*/
calculateVotingPower (user, numberDecimals = 2) {
const secondsago = (new Date() - new Date(user.last_vote_time + 'Z')) / 1000
const vpow = user.voting_power + (10000 * secondsago / 432000)
return Math.min(vpow / 100, 100).toFixed(numberDecimals)
}
/**
* calculate the bandwidth information of a user
* @param {JSON} user a user object
* @param {Number} numberDecimals number of decimals to return, default 2
* @param {Boolean} refreshSteemProperties default true, refresh the Steem properties (Steem rate, etc...)
* @returns {JSON} json with the bandwidth information (used, allocated in percents and bytes)
* @example
* easysteem.me()
* .then(user => {
* easysteem.calculateBandwidth(user.account, 3)
* .then(result => console.log(result))
* })
* .catch(error => console.error(error.error, error.error_description))
* /*{
* bytes: {
* allocated: "8.106 MB",
* remaining: "8.088 MB",
* used: "18.421 KB"
* },
* percents: {
* remaining: "99.778",
* used: "0.222"
* }
* }*\/
*/
calculateBandwidth (user, numberDecimals = 2, refreshSteemProperties = true) {
return new Promise(async resolve => {
if (!this.steemProperties || refreshSteemProperties) {
await this.refreshSteemProperties()
}
const STEEMIT_BANDWIDTH_AVERAGE_WINDOW_SECONDS = 60 * 60 * 24 * 7
let vestingShares = parseFloat(this.parsePayoutAmount(user.vesting_shares))
let receivedVestingShares = parseFloat(this.parsePayoutAmount(user.received_vesting_shares))
let averageBandwidth = parseInt(user.average_bandwidth, 10)
let deltaTime = (new Date() - new Date(user.last_bandwidth_update + 'Z')) / 1000
let bandwidthAllocated = (this.steemProperties.maxVirtualBandwidth * (vestingShares + receivedVestingShares) / this.steemProperties.totalVestingShares)
bandwidthAllocated = Math.round(bandwidthAllocated / 1000000)
let newBandwidth = 0
if (deltaTime < STEEMIT_BANDWIDTH_AVERAGE_WINDOW_SECONDS) {
newBandwidth = (((STEEMIT_BANDWIDTH_AVERAGE_WINDOW_SECONDS - deltaTime) * averageBandwidth) / STEEMIT_BANDWIDTH_AVERAGE_WINDOW_SECONDS)
}
newBandwidth = Math.round(newBandwidth / 1000000)
resolve({
'percents': {
'remaining': (100 - (100 * newBandwidth / bandwidthAllocated)).toFixed(numberDecimals),
'used': (100 * newBandwidth / bandwidthAllocated).toFixed(numberDecimals)
},
'bytes': {
'remaining': this.bytesToSize(bandwidthAllocated - newBandwidth, numberDecimals),
'used': this.bytesToSize(newBandwidth, numberDecimals),
'allocated': this.bytesToSize(bandwidthAllocated, numberDecimals)
}
})
})
}
/**
* calculate the reputation in a more readable way
* @param {JSON} user a user object
* @param {*} numberDecimals number of decimals to return, default 2
* @returns {Number} return the calculated reputation
*/
calculateReputation (rawReputation, numberDecimals = 2) {
const isNegative = (rawReputation < 0)
let reputation = Math.log10(Math.abs(rawReputation))
reputation = Math.max(reputation - 9, 0)
reputation *= isNegative ? -9 : 9
reputation += 25
return reputation.toFixed(numberDecimals)
}
/**
* calculate an estimation of an account value
* @param {JSON} user a user object
* @param {Number} numberDecimals default 2, number of decimals to return
* @param {Boolean} refreshSteemProperties default true, refresh the Steem properties (Steem rate, etc...)
*/
calculateEstimatedAccountValue (
user,
numberDecimals = 2,
refreshSteemProperties = true
) {
return new Promise(async resolve => {
if (!this.steemProperties || refreshSteemProperties) {
await this.refreshSteemProperties()
}
const steemPower = this.vestToSteem(
this.parsePayoutAmount(user.vesting_shares),
this.steemProperties.totalVestingShares,
this.steemProperties.totalVestingFund
)
resolve(
(parseFloat(this.steemProperties.steemRate) * (parseFloat(user.balance) + parseFloat(steemPower)) +
parseFloat(user.sbd_balance) * parseFloat(this.steemProperties.sbdRate)).toFixed(numberDecimals)
)
})
}
/**
* convert an amount of vestings into an amount of Steem
* @param {Number} vestingShares
* @param {Number} totalVestingShares
* @param {Number} totalVestingFundSteem
*/
vestToSteem (vestingShares, totalVestingShares, totalVestingFundSteem) {
return (
parseFloat(totalVestingFundSteem) *
(parseFloat(vestingShares) / parseFloat(totalVestingShares))
)
}
/**
* get the USD price of a crypto currency
* @param {String} currency a cryptocompare compatible crypto currency code
* @returns {Number} the USD price of the crypto currency
*/
getCryptoCurrencyPrice (currency) {
return new Promise(async resolve => {
fetch(`https://min-api.cryptocompare.com/data/price?fsym=${currency}&tsyms=USD`) // eslint-disable-line no-undef
.then(async res => {
const json = await res.json()
resolve(json.USD)
})
})
}
/**
* orders the votes
* @param {Array<JSON>} votes array containing the votes
* @param {String} orderBy default EasySteem.ORDER_OPTIONS.PAYOUT but REPUTATION, PERCENT, PAYOUT available
*/
orderVotes (votes, orderBy = EasySteem.ORDER_OPTIONS.PAYOUT) {
return new Promise(async resolve => {
if (!this.steemProperties && orderBy === EasySteem.ORDER_OPTIONS.PAYOUT) {
await this.refreshSteemProperties()
}
if (votes.length > 1) {
votes.sort((a, b) => {
switch (orderBy) {
case EasySteem.ORDER_OPTIONS.PAYOUT:
let votePayoutA = this.sharesToSteem(a.rshares)
a.payout = votePayoutA.toFixed(3)
let votePayoutB = this.sharesToSteem(b.rshares)
b.payout = votePayoutB.toFixed(3)
return votePayoutA > votePayoutB ? -1 : votePayoutA < votePayoutB ? 1 : 0
case EasySteem.ORDER_OPTIONS.REPUTATION:
let voteReputationA = this.calculateReputation(a.reputation)
a.formattedReputation = voteReputationA
let voteReputationB = this.calculateReputation(b.reputation)
b.formattedReputation = voteReputationB
return voteReputationA > voteReputationB ? -1 : voteReputationA < voteReputationB ? 1 : 0
case EasySteem.ORDER_OPTIONS.PERCENT:
let votePercentA = a.percent
a.formattedPercent = votePercentA / 100
let votePercentB = b.percent
b.formattedPercent = votePercentB / 100
return votePercentA > votePercentB ? -1 : votePercentA < votePercentB ? 1 : 0
}
})
} else if (votes.length > 0) {
switch (orderBy) {
case EasySteem.ORDER_OPTIONS.PAYOUT:
votes[0].votePayout = this.sharesToSteem(votes[0].rshares).toFixed(3)
break
case EasySteem.ORDER_OPTIONS.REPUTATION:
votes[0].voteReputation = this.calculateReputation(votes[0].reputation)
break
default:
}
}
resolve()
})
}
/**
* orders the comments
* @param {Array<JSON>} votes array containing the comments
* @param {String} orderBy default EasySteem.ORDER_OPTIONS.PAYOUT but OLDEST, NEWEST, REPUTATION, PAYOUT available
*/
orderComments (comments, orderBy = EasySteem.ORDER_OPTIONS.PAYOUT) {
return new Promise(async resolve => {
if (!this.steemProperties && orderBy === EasySteem.ORDER_OPTIONS.PAYOUT) {
await this.refreshSteemProperties()
}
if (comments.length > 1) {
comments.sort((a, b) => {
switch (orderBy) {
case EasySteem.ORDER_OPTIONS.NEWEST:
a = new Date(a.created)
b = new Date(b.created)
return a > b ? -1 : a < b ? 1 : 0
case EasySteem.ORDER_OPTIONS.OLDEST:
a = new Date(a.created)
b = new Date(b.created)
return a < b ? -1 : a > b ? 1 : 0
case EasySteem.ORDER_OPTIONS.PAYOUT:
let commentPayoutA = this.parsePayoutAmount(a.pending_payout_value) === 0 ? this.parsePayoutAmount(a.total_payout_value) + this.parsePayoutAmount(a.curator_payout_value) : this.parsePayoutAmount(a.pending_payout_value)
a.formattedTotalPayout = commentPayoutA
let commentPayoutB = this.parsePayoutAmount(b.pending_payout_value) === 0 ? this.parsePayoutAmount(b.total_payout_value) + this.parsePayoutAmount(b.curator_payout_value) : this.parsePayoutAmount(b.pending_payout_value)
b.formattedTotalPayout = commentPayoutB
return commentPayoutA > commentPayoutB ? -1 : commentPayoutA < commentPayoutB ? 1 : 0
case EasySteem.ORDER_OPTIONS.REPUTATION:
let commentReputationA = this.calculateReputation(a.author_reputation)
a.formattedReputation = commentReputationA
let commentReputationB = this.calculateReputation(b.author_reputation)
b.formattedReputation = commentReputationB
return commentReputationA > commentReputationB ? -1 : commentReputationA < commentReputationB ? 1 : 0
}
})
} else if (comments.length > 0) {
switch (orderBy) {
case EasySteem.ORDER_OPTIONS.REPUTATION:
comments[0].formattedReputation = this.calculateReputation(comments[0].author_reputation)
break
default:
}
}
resolve()
})
}
/**
* converts a number of shares into a number of Steem
* @param {Number} rshares number of shares
* @returns the shares converted into a Steem value
*/
sharesToSteem (shares) {
return shares * this.steemProperties.rewardBalance / this.steemProperties.recentClaims * this.steemProperties.steemRate
}
/**
* convert a number of bytes into a readable size (1B, 1KB, 1MB, ...)
* @param {Number} bytes the bytes to convert
* @param {Number} numberDecimals default 2, number of decimals to return
* @return {String} formated string representing the size
*/
bytesToSize (bytes, numberDecimals) {
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
if (bytes === 0) return 'n/a'
const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10)
if (i === 0) return `${bytes} ${sizes[i]})`
return `${(bytes / (1024 ** i)).toFixed(numberDecimals)} ${sizes[i]}`
}
}