Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Feature) Top delegates #39

Merged
merged 28 commits into from
Jul 2, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
59d1ffa
Implements top x of transcoders
Agupane Jun 11, 2019
af54b15
Fixs sort operation
Agupane Jun 11, 2019
db61fd8
Optimize top delegates
Agupane Jun 11, 2019
d9a187d
Orders rewards in desc, fix reward calls calculation
Agupane Jun 12, 2019
f8031a8
Merge branch 'feature/delegateAPI' into feature/topDelegates
Agupane Jun 12, 2019
643c383
Moves calculateRoi and calculateMissedRewardCalls to utils file
Agupane Jun 13, 2019
1082be9
Moves calculateRoi and calculateMissedRewardCalls to utils file
Agupane Jun 13, 2019
5302b14
Merge feature/topDelegates with development
Agupane Jun 24, 2019
e976180
Removes unused file
Agupane Jun 24, 2019
fdd22eb
Implements getTopDelegates by LPT RETURN
Agupane Jun 24, 2019
8e913dd
Merge branch 'feature/cacheSDK' into feature/topDelegates
Agupane Jun 24, 2019
19b2bb8
Merge branch 'feature/cacheSDK' into feature/topDelegates
Agupane Jun 24, 2019
3b37a9e
Fixs MathBN utils imports
Agupane Jun 24, 2019
44f3b48
Merge branch 'feature/sdk-graphql-refactoring' into feature/topDelegates
Agupane Jun 25, 2019
89bb605
Merge branch 'feature/sdk-graphql-refactoring' into feature/topDelegates
Agupane Jun 25, 2019
ff1f951
Removes console logs
Agupane Jun 25, 2019
f3c00f1
Forces the tests return exit code after finish
Agupane Jun 25, 2019
c97165d
Adds simulateNextReturnForGivenDelegatorStakedAmount function on dele…
Agupane Jun 25, 2019
33be624
Merge branch 'feature/sdk-graphql-refactoring' into feature/topDelegates
Agupane Jun 25, 2019
9d9b449
Removes unused getDelegatorNextReturn function
Agupane Jun 26, 2019
e5b9a8a
Top delegates tests
Agupane Jun 26, 2019
f7dcde8
Fixs tests
Agupane Jun 27, 2019
092c2f8
Fixs conflicts
Agupane Jul 1, 2019
2abfe7c
Merge branch 'development' into feature/topDelegates
Agupane Jul 2, 2019
a04e323
Fixs tests
Agupane Jul 2, 2019
f7aa365
Refactor code
Jul 2, 2019
6f65bc0
Update postman
Jul 2, 2019
17f2760
Merge branch 'feature/topDelegates' of github.com:protofire/livepeer-…
Jul 2, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/constants.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const PROTOCOL_DIVISION_BASE = 1000000
const TOKEN_DECIMALS_MULTIPLIER = 1000000000000000000
const CACHE_UPDATE_INTERVAL = 1000 * 60 * 60 * 2 // Updates every 2 hours
const TOKEN_DECIMAL_UNITS = 18

module.exports = {
PROTOCOL_DIVISION_BASE,
TOKEN_DECIMALS_MULTIPLIER,
TOKEN_DECIMAL_UNITS,
CACHE_UPDATE_INTERVAL
}
9 changes: 9 additions & 0 deletions config/param-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,5 +75,14 @@ module.exports = {
params: {
address: Joi.string().required()
}
},

// GET /api/delegates/top/:number
getTopDelegates: {
params: {
number: Joi.number()
.min(0)
.required()
}
}
}
29 changes: 8 additions & 21 deletions server/delegate/delegate.controller.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/**
/**
* Handler for delegates
* @returns {Delegate}
*/

const { MathBN, tokenAmountInUnits } = require('../helpers/utils')
const BN = require('bn.js')
const { getDelegateService } = require('../helpers/services/delegateService')

const getByAddress = async (req, res, next) => {
Expand All @@ -17,30 +15,19 @@ const getByAddress = async (req, res, next) => {
next(error)
}
}
const getROI = async (req, res, next) => {
const { address } = req.params
const delegateService = getDelegateService()
try {
const rewards = await delegateService.getDelegateRewards(address)
const totalStake = await delegateService.getDelegateTotalStake(address)
let roi = null
if (rewards && totalStake) {
const totalReward = rewards.reduce((total, reward) => {
// Removes the cases in which the rewardToken is null
const rewardTokenAmount = reward.rewardTokens ? reward.rewardTokens : 0
const amount = tokenAmountInUnits(rewardTokenAmount)
return MathBN.add(total, amount)
}, new BN(0))
roi = MathBN.div(totalReward, totalStake)
}

res.json({ roi })
const topDelegates = async (req, res, next) => {
const { number } = req.params
try {
const delegateService = getDelegateService()
let result = await delegateService.getTopDelegates(number)
res.json(result)
} catch (error) {
next(error)
}
}

module.exports = {
getByAddress,
getROI
topDelegates
}
8 changes: 4 additions & 4 deletions server/delegate/delegate.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ const validate = require('express-validation')
const paramValidation = require('../../config/param-validation')

const router = express.Router() // eslint-disable-line new-cap
const { getByAddress, getROI } = require('./delegate.controller')
const { getByAddress, topDelegates } = require('./delegate.controller')

/** GET /api/delegates/address/:address - Get delegate by address */
router
.route('/address/:address')

.get(validate(paramValidation.getByAddress), getByAddress)

/** GET /api/delegates/roi/:address - Get delegate ROI by address */
/** GET /api/delegates/top/:number - Get the top of delegates by ROI */
router
.route('/roi/:address')
.route('/top/:number')

.get(validate(paramValidation.getByAddress), getROI)
.get(validate(paramValidation.getTopDelegates), topDelegates)

module.exports = router
4 changes: 3 additions & 1 deletion server/helpers/graphql/queries/delegate.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const getDelegateSummary = async delegateAddress => {
{
transcoder(id: "${delegateAddress}") {
id,
address: id,
active,
ensName,
status,
Expand All @@ -33,8 +34,9 @@ const getRegisteredDelegates = async () => {
const queryResult = await client.query({
query: gql`
{
transcoders(where: { totalStake_gt: 0, status: "Registered", id_not: null }) {
transcoders(where: { totalStake_gt: 0, status: "Registered", id_not: null, active: true }) {
id
address: id
totalStake
rewards {
id
Expand Down
72 changes: 69 additions & 3 deletions server/helpers/services/delegateService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ const promiseRetry = require('promise-retry')
const _ = require('lodash')

const { getProtocolService } = require('./protocolService')
const { MathBN, tokenAmountInUnits, calculateMissedRewardCalls } = require('../utils')
const {
MathBN,
tokenAmountInUnits,
unitAmountInTokenUnits,
calculateMissedRewardCalls
} = require('../utils')
const { PROTOCOL_DIVISION_BASE } = require('../../../config/constants')

let delegateServiceInstance
Expand Down Expand Up @@ -71,11 +76,11 @@ class DelegateService {
// For a given delegateAddress return the next reward that will be distributed towards delegators
getDelegateRewardToDelegators = async delegateAddress => {
// FORMULA: DelegateRewardToDelegators = DelegateProtocolNextReward - DelegateProtocolNextReward * rewardCut
let [summary, protocolNextReward] = await Promise.all([
let [delegate, protocolNextReward] = await Promise.all([
this.getDelegate(delegateAddress),
this.getDelegateProtocolNextReward(delegateAddress)
])
const { pendingRewardCut } = summary
const { pendingRewardCut } = delegate
const rewardCut = MathBN.div(pendingRewardCut, PROTOCOL_DIVISION_BASE)
const rewardToDelegate = MathBN.mul(protocolNextReward, rewardCut)
return MathBN.sub(protocolNextReward, rewardToDelegate)
Expand Down Expand Up @@ -104,6 +109,67 @@ class DelegateService {
return totalStake
}

// Returns an array of all the active and registered delegates
getRegisteredDelegates = async () => {
const { getRegisteredDelegates } = this.source
return await getRegisteredDelegates()
}

// Calculates how much of lptTokenRewards a delegator will obtain from a given delegator
// Receives a amount of staked LPT (delegatorAmountToStake), the totalStake of the delegate and the rewardsToDelegators amount
// Note: delegatorAmountToStake should be on tokenUnits
// All the values should be in tokenValues (18 decimals)
simulateNextReturnForGivenDelegatorStakedAmount = (
rewardsToDelegatorsInTokens,
delegateTotalStakeInTokens,
delegatorAmountToStakeInTokens
) => {
const rewardsToDelegators = tokenAmountInUnits(rewardsToDelegatorsInTokens)
const delegateTotalStake = tokenAmountInUnits(delegateTotalStakeInTokens)
const delegatorAmountToStake = tokenAmountInUnits(delegatorAmountToStakeInTokens)
// Checks that the delegatorStakedAmount is <= delegateTotalStake
if (MathBN.lte(delegatorAmountToStake, delegateTotalStake)) {
// Calculates the delegatorParticipation in the totalStake
// FORMULA: delegatorStakedAmount / delegateTotalStake
const participationInTotalStakeRatio = MathBN.div(delegatorAmountToStake, delegateTotalStake)
// Then calculates the reward with FORMULA: participationInTotalStakeRatio * rewardToDelegators
const result = MathBN.mul(rewardsToDelegators, participationInTotalStakeRatio)
return MathBN.mul(rewardsToDelegators, participationInTotalStakeRatio)
} else {
return 0
}
}

// Returns the best active and registered <topNumber> delegates based on the return that they will provide on 1000 bonded LPT
getTopDelegates = async (topNumber, amountToStake = 1000) => {
let topDelegates = []
const delegates = await this.getRegisteredDelegates()
const amountToStakeInTokens = unitAmountInTokenUnits(amountToStake)
for (let delegateIterator of delegates) {
const rewardsToDelegators = await this.getDelegateRewardToDelegators(delegateIterator.address)
const rewardsConverted = unitAmountInTokenUnits(rewardsToDelegators)
// Best return formula = order delegates by the best amount of return that will be given towards bonded delegators
const roi = this.simulateNextReturnForGivenDelegatorStakedAmount(
rewardsConverted,
delegateIterator.totalStake,
amountToStakeInTokens
)
const totalStake = tokenAmountInUnits(delegateIterator.totalStake)
topDelegates.push({
id: delegateIterator.id,
totalStake,
roi
})
}
// Sorts in ROI descending order
topDelegates.sort((a, b) => {
const aBn = MathBN.toBig(a.roi)
const bBn = MathBN.toBig(b.roi)
return bBn.sub(aBn)
})
return topDelegates.slice(0, topNumber)
}

getDelegates = async () => {
const { getLivepeerTranscoders } = this.source
const delegates = await getLivepeerTranscoders()
Expand Down
21 changes: 16 additions & 5 deletions server/helpers/test/util.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
const createRewardObject = (transcoderId, roundId) => {
const { unitAmountInTokenUnits } = require('../utils')

const createRewardObject = (transcoderId, roundId, rewardAmount) => {
// ID between 1 and 100
const id = Math.floor(Math.random() * 100 + 1)
// Reward token between 1*10.pow(21) and 99 * 10.pow(21)
const base = Math.pow(10, 21)
const top = 9 * Math.pow(10, 21)
const rewardTokens = Math.floor(Math.random() * top + base).toString()
const rewardTokens = rewardAmount
? rewardAmount
: Math.floor(Math.random() * top + base).toString()
const transcoder = createTranscoder(transcoderId)
const round = createRoundObject(roundId)
return {
Expand All @@ -22,9 +26,10 @@ const createRoundObject = roundId => {
}

// TODO Complete with all the fields
const createTranscoder = transcoderId => {
const createTranscoder = (transcoderId, totalStake = '440522208151278163711606') => {
const mockTranscoder = {
id: transcoderId,
address: transcoderId,
active: true,
ensName: null,
status: 'Registered',
Expand All @@ -35,7 +40,7 @@ const createTranscoder = transcoderId => {
pendingRewardCut: '50000',
pendingFeeShare: '450000',
pendingPricePerSegment: '150000000000',
totalStake: '440522208151278163711606'
totalStake
}
return mockTranscoder
}
Expand All @@ -62,9 +67,15 @@ const createDelegator = delegatorId => {
return mockDelegator
}

const createTotalStake = (min, max) => {
const stakeInUnits = Math.floor(Math.random() * max) + min
return unitAmountInTokenUnits(stakeInUnits)
}

module.exports = {
createRewardObject,
createRoundObject,
createTranscoder,
createDelegator
createDelegator,
createTotalStake
}
12 changes: 8 additions & 4 deletions server/helpers/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { TOKEN_DECIMALS_MULTIPLIER } = require('../../config/constants')
const { TOKEN_DECIMAL_UNITS } = require('../../config/constants')

const Big = require('big.js')
const BN = require('bn.js')
Expand Down Expand Up @@ -263,13 +263,17 @@ const getDelegatorRoundsUntilUnbonded = data => {
: 0
}

const tokenAmountInUnits = (amount, decimals = 18) => {
const tokenAmountInUnits = (amount, decimals = TOKEN_DECIMAL_UNITS) => {
if (!amount) {
return 0
}
const decimalsPerToken = MathBN.pow(10, decimals)
return MathBN.div(amount, decimalsPerToken)
}

const unitAmountInTokenUnits = amount => {
return MathBN.mul(amount, TOKEN_DECIMALS_MULTIPLIER)
const unitAmountInTokenUnits = (amount, decimals = TOKEN_DECIMAL_UNITS) => {
const decimalsPerToken = MathBN.pow(10, decimals)
return MathBN.mul(amount, decimalsPerToken)
}

const calculateMissedRewardCalls = (rewards, currentRound) => {
Expand Down
Loading