Skip to content

Commit

Permalink
Add estimated rewards calculation to the API.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jiri Malek committed Apr 30, 2020
1 parent b911f92 commit ba0bbe4
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 8 deletions.
164 changes: 164 additions & 0 deletions internal/graphql/resolvers/estimated_rewards.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package resolvers

import (
"fantom-api-graphql/internal/repository"
"fantom-api-graphql/internal/types"
"fmt"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"math/big"
)

// constants used in reward calculations
const (
erwSecondsInDay uint64 = 86400
erwSecondsInWeek uint64 = 86400 * 7
erwSecondsInYear uint64 = 31556926
erwSecondsInMonth = erwSecondsInYear / 12
)

// EstimatedRewards represents resolvable estimated rewards structure.
type EstimatedRewards struct {
repo repository.Repository
Staked hexutil.Uint64
TotalStaked hexutil.Big
LastEpoch types.Epoch
}

// NewEstimatedRewards builds new resolvable estimated rewards structure.
func NewEstimatedRewards(ep *types.Epoch, amount *hexutil.Uint64, total *hexutil.Big, repo repository.Repository) EstimatedRewards {
return EstimatedRewards{
repo: repo,
Staked: *amount,
TotalStaked: *total,
LastEpoch: *ep,
}
}

// estimateRewardsByAddress instantiates the estimated rewards for specified address if possible.
func (rs *rootResolver) estimateRewardsByAddress(addr *common.Address, ep *types.Epoch, total *hexutil.Big) (EstimatedRewards, error) {
// try to get the address involved
acc, err := rs.repo.Account(addr)
if err != nil {
// log issue and return empty data
rs.log.Error("invalid address or address not found")
return EstimatedRewards{}, fmt.Errorf("address not found")
}

// inform to debug
rs.log.Debugf("calculating rewards estimation for address [%s]", acc.Address.String())

// get the address balance
balance, err := rs.repo.AccountBalance(acc)
if err != nil {
// log issue and return empty data
rs.log.Errorf("can not get balance for address [%s]", acc.Address.String())
return EstimatedRewards{}, fmt.Errorf("address balance not found")
}

// get the value of the balance as Uint64 value
val := hexutil.Uint64(new(big.Int).Div(balance.ToInt(), new(big.Int).SetUint64(1000000000000000000)).Uint64())
return NewEstimatedRewards(ep, &val, total, rs.repo), nil
}

// EstimateRewards resolves reward estimation for the given address or amount staked.
func (rs *rootResolver) EstimateRewards(args *struct {
Address *common.Address
Amount *hexutil.Uint64
}) (EstimatedRewards, error) {
// at least one of the parameters must be present
if args == nil || (args.Address == nil && args.Amount == nil) {
// log issue and return empty data
rs.log.Error("can not calculate estimated rewards without parameters")
return EstimatedRewards{}, fmt.Errorf("missing both address and amount")
}

// get the latest sealed epoch
// the data could be delayed behind the real-time sealed epoch due to caching,
// but we don't need that precise reflection here
ep, err := rs.repo.CurrentSealedEpoch()
if err != nil {
// log issue and return empty data
rs.log.Errorf("can not get the current sealed epoch information; %s", err.Error())
return EstimatedRewards{}, fmt.Errorf("current sealed epoch not found")
}

// get the current total staked amount
total, err := rs.repo.TotalStaked()
if err != nil {
// log issue and return empty data
rs.log.Errorf("can not get the current total staked amount; %s", err.Error())
return EstimatedRewards{}, fmt.Errorf("current total staked amount not found")
}

// if address is specified, pull the estimation from it
if args.Address != nil {
return rs.estimateRewardsByAddress(args.Address, ep, total)
}

// get the value directly from the provided amount
return NewEstimatedRewards(ep, args.Amount, total, rs.repo), nil
}

// canCalculateRewards checks if the reward can actually be calculated
func (erw EstimatedRewards) canCalculateRewards() bool {
zero := new(big.Int)
return erw.Staked > 0 &&
erw.LastEpoch.BaseRewardPerSecond.ToInt().Cmp(zero) > 0 &&
erw.TotalStaked.ToInt().Cmp(zero) > 0
}

// getRewards calculates the rewards value for the given time period.
func (erw EstimatedRewards) getRewards(period uint64) hexutil.Big {
// validate that we can actually calculate the value
if !erw.canCalculateRewards() {
fmt.Printf("can not calculate!")
return hexutil.Big{}
}

// prep values and calculate results
// (perSecond * period * stakedAmount) / totalStakedAmount
base := new(big.Int).Mul(erw.LastEpoch.BaseRewardPerSecond.ToInt(), new(big.Int).SetUint64(period))
staked := new(big.Int).Mul(base, new(big.Int).SetUint64(uint64(erw.Staked)))
val := new(big.Int).Div(staked, erw.TotalStaked.ToInt())

// return the value
return hexutil.Big(*val)
}

// DailyReward calculates daily rewards for the given rewards estimation.
func (erw EstimatedRewards) DailyReward() hexutil.Big {
return erw.getRewards(erwSecondsInDay)
}

// WeeklyReward calculates daily rewards for the given rewards estimation.
func (erw EstimatedRewards) WeeklyReward() hexutil.Big {
return erw.getRewards(erwSecondsInWeek)
}

// MonthlyReward calculates daily rewards for the given rewards estimation.
func (erw EstimatedRewards) MonthlyReward() hexutil.Big {
return erw.getRewards(erwSecondsInMonth)
}

// YearlyReward calculates daily rewards for the given rewards estimation.
func (erw EstimatedRewards) YearlyReward() hexutil.Big {
return erw.getRewards(erwSecondsInYear)
}

// CurrentRewardRateYearly calculates average reward rate
// for any staked amount in average per year
func (erw EstimatedRewards) CurrentRewardRateYearly() int32 {
// validate that we can actually calculate the value
if !erw.canCalculateRewards() {
return 0
}

// prep and calculate the rate
base := new(big.Int).Mul(erw.LastEpoch.BaseRewardPerSecond.ToInt(), new(big.Int).SetUint64(erwSecondsInYear))
toPct := new(big.Int).Mul(base, new(big.Int).SetUint64(100))
val := new(big.Int).Div(toPct, erw.TotalStaked.ToInt())

// get the value
return int32(val.Int64())
}
6 changes: 6 additions & 0 deletions internal/graphql/resolvers/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ type ApiResolver interface {
// GasPrice resolves the current amount of WEI for single Gas.
GasPrice() (hexutil.Uint64, error)

// EstimateRewards resolves reward estimation for the given address or amount staked.
EstimateRewards(*struct {
Address *common.Address
Amount *hexutil.Uint64
}) (EstimatedRewards, error)

// SendTransaction sends raw signed and RLP encoded transaction to the block chain.
SendTransaction(*struct{ Tx hexutil.Bytes }) (*Transaction, error)

Expand Down
47 changes: 46 additions & 1 deletion internal/graphql/schema/bundle.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package gqlschema

// Auto generated GraphQL schema bundle; created 2020-04-16 18:54
// Auto generated GraphQL schema bundle; created 2020-05-01 00:34
const schema = `
# StakerInfo represents extended staker information from smart contract.
type StakerInfo {
Expand Down Expand Up @@ -94,6 +94,44 @@ type Account {
delegation: Delegator
}
# EstimatedRewards represents a calculated rewards etimation for an account or amount staked
type EstimatedRewards {
"Amount of FTM tokens expected to be staked for the calculation."
staked: Long!
"dailyReward represents amount of FTM tokens estimated to be rewarded for staked amount in average per day."
dailyReward: BigInt!
"weeklyReward represents amount of FTM tokens estimated to be rewarded for staked amount in average per week."
weeklyReward: BigInt!
"monthlyReward represents amount of FTM tokens estimated to be rewarded for staked amount in average per month."
monthlyReward: BigInt!
"yearlyReward represents amount of FTM tokens estimated to be rewarded for staked amount in average per year."
yearlyReward: BigInt!
"""
currentRewardYearRate represents average reward rate for any staked amount in average per year.
The value is calculated as linear gross proceeds for staked amount of tokens yearly.
"""
currentRewardRateYearly: Int!
"""
Total amount of staked FTM tokens used for the calculation.
The estimation uses total staked amount, not the effective amount provided
by the last epoch. The effective amount does include current undelegations and also
skips offline self-stakings and flagged stakings.
"""
totalStaked: BigInt!
"""
Information about the last sealed epoch of the Opera blockchain.
The epoch provides useful information about total FTM supply,
total amount staked, rewards rate and weight, fee, etc.
"""
lastEpoch: Epoch!
}
# TransactionList is a list of transaction edges provided by sequential access request.
type TransactionList {
# Edges contains provided edges of the sequential list.
Expand Down Expand Up @@ -480,6 +518,13 @@ type Query {
"Get price details of the Opera blockchain token for the given target symbols."
price(to:String!):Price!
"""
Get calculated staking rewards for an account or given staking amount.
At least one of the address and amount parameters must be provided.
If you provide both, the address takes precedence and the amount is ignored.
"""
estimateRewards(address:Address, amount:Long):EstimatedRewards!
}
# Mutation andpoints for modifying the data
Expand Down
12 changes: 10 additions & 2 deletions internal/graphql/schema/definition/.graphqlconfig
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"Local GraphQL Endpoint": {
"url": "http://localhost:16761/api",
"headers": {
"user-agent": "JS GraphQL"
"user-agent": "JS GraphQL",
"origin": "http://localhost"
},
"introspect": false
},
Expand All @@ -18,7 +19,14 @@
"introspect": false
},
"Remote GraphQL Endpoint": {
"url": "https://fantom.rocks/fapi",
"url": "https://api.fantom.rocks/api",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": false
},
"Remote GraphQL Endpoint API2": {
"url": "https://api2.fantom.rocks/graphql",
"headers": {
"user-agent": "JS GraphQL"
},
Expand Down
5 changes: 3 additions & 2 deletions internal/graphql/schema/definition/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,11 @@ type Query {
price(to:String!):Price!

"""
Get calculated staking rewards for an account or given staking amount.
Get calculated staking rewards for an account or given staking amount in FTM tokens.
At least one of the address and amount parameters must be provided.
If you provide both, the address takes precedence and the amount is ignored.
"""
estimateRewards(address:Address, amount:BigInt!):EstimatedRewards!
estimateRewards(address:Address, amount:Long):EstimatedRewards!
}

# Mutation andpoints for modifying the data
Expand Down
26 changes: 23 additions & 3 deletions internal/graphql/schema/definition/types/estimated_rewards.graphql
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
# EstimatedRewards represents a calculated rewards etimation for an account or amount staked
type EstimatedRewards {
"Amount of FTM tokens expected to be staked for the calculation."
staked: BigInt!
staked: Long!

"monthlyReward represents amount of FTM tokens estimated to be rewarded in average for staked amount per month."
"dailyReward represents amount of FTM tokens estimated to be rewarded for staked amount in average per day."
dailyReward: BigInt!

"weeklyReward represents amount of FTM tokens estimated to be rewarded for staked amount in average per week."
weeklyReward: BigInt!

"monthlyReward represents amount of FTM tokens estimated to be rewarded for staked amount in average per month."
monthlyReward: BigInt!

"yearlyReward represents amount of FTM tokens estimated to be rewarded in average for staked amount per year."
"yearlyReward represents amount of FTM tokens estimated to be rewarded for staked amount in average per year."
yearlyReward: BigInt!

"""
currentRewardYearRate represents average reward rate for any staked amount in average per year.
The value is calculated as linear gross proceeds for staked amount of tokens yearly.
"""
currentRewardRateYearly: Int!

"""
Total amount of staked FTM tokens used for the calculation in WEI units.
The estimation uses total staked amount, not the effective amount provided
by the last epoch. The effective amount does include current undelegations and also
skips offline self-stakings and flagged stakings.
"""
totalStaked: BigInt!

"""
Information about the last sealed epoch of the Opera blockchain.
The epoch provides useful information about total FTM supply,
Expand Down
46 changes: 46 additions & 0 deletions internal/repository/cache/epoch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package cache

import (
"fantom-api-graphql/internal/types"
"fmt"
)

// lastEpochCacheKey represents the in-memory cache key for the latest sealed Epoch data.
const lastEpochCacheKey = "last-sealed-epoch"

// PullLastEpoch extracts information about the latest Epoch from the in-memory cache if available.
func (b *MemBridge) PullLastEpoch() *types.Epoch {
// try to get the Epoch data from the cache
data, err := b.cache.Get(lastEpochCacheKey)
if err != nil {
// cache returns ErrEntryNotFound if the key does not exist
return nil
}

// do we have the data?
ep, err := types.UnmarshalEpoch(data)
if err != nil {
b.log.Criticalf("can not decode epoch data from in-memory cache; %s", err.Error())
return nil
}

return ep
}

// PushLastEpoch stores provided latest sealed Epoch in the in-memory cache.
func (b *MemBridge) PushLastEpoch(ep *types.Epoch) error {
// we need valid account
if nil == ep {
return fmt.Errorf("undefined epoch can not be pushed to the in-memory cache")
}

// encode account
data, err := ep.Marshal()
if err != nil {
b.log.Criticalf("can not marshal epoch to JSON; %s", err.Error())
return err
}

// set the data to cache by block number
return b.cache.Set(lastEpochCacheKey, data)
}
Loading

0 comments on commit ba0bbe4

Please sign in to comment.