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

Integrated Staking Reward Calculator feature #1977

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions cmd/dcrdata/internal/api/apirouter.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ func NewAPIRouter(app *appContext, JSONIndent string, useRealIP, compressLarge b
r.With(m.ChartTypeCtx).Get("/{charttype}", app.ChartTypeData)
})

mux.Route("/stakingcalc", func(r chi.Router) {
r.Get("/get-future-reward", app.getStakeRewardCalc)
})

mux.Route("/ticketpool", func(r chi.Router) {
r.Get("/", app.getTicketPoolByDate)
r.With(m.TicketPoolCtx).Get("/bydate/{tp}", app.getTicketPoolByDate)
Expand Down
204 changes: 204 additions & 0 deletions cmd/dcrdata/internal/api/apiroutes.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"fmt"
"html"
"io"
"math"
"net/http"
"reflect"
"sort"
Expand Down Expand Up @@ -141,6 +142,17 @@ type AppContextConfig struct {
AppVer string
}

type simulationRow struct {
SimBlock float64 `json:"height"`
SimDay int `json:"day"`
TicketPrice float64 `json:"ticket_price"`
MatrueTickets float64 `json:"matured_tickets"`
DCRBalance float64 `json:"dcr_balance"`
TicketsPurchased float64 `json:"tickets_purchased"`
Reward float64 `json:"reward"`
ReturnedFund float64 `json:"returned_fund"`
}

// NewContext constructs a new appContext from the RPC client and database, and
// JSON indentation string.
func NewContext(cfg *AppContextConfig) *appContext {
Expand Down Expand Up @@ -680,6 +692,78 @@ func (c *appContext) getTransactionHex(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, hex)
}

func (c *appContext) getStakeRewardCalc(w http.ResponseWriter, r *http.Request) {
// Get parameters. Contain Amount, StartDate and EndDate
startingBalance, err := strconv.ParseFloat(r.URL.Query().Get("startingBalance"), 64)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
return
}
startDateUnix, err := strconv.ParseInt(r.URL.Query().Get("startDate"), 10, 64)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
return
}
endDateUnix, err := strconv.ParseInt(r.URL.Query().Get("endDate"), 10, 64)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError)
return
}

//Get ticket price (Stake Difficulty)
ticketPriceInput := c.DataSource.GetStakeDiffEstimates().CurrentStakeDifficulty
//Ticket Pool Info
ticketPoolValue := c.DataSource.GetPoolInfo(int(c.DataSource.Height())).Value
coinSupply := c.DataSource.CurrentCoinSupply()

if coinSupply == nil {
apiLog.Error("Unable to get coin supply.")
http.Error(w, http.StatusText(422), 422)
return
}

//Get coin supply value
var coinSupplyTmp = dcrutil.Amount(coinSupply.Mined).ToCoin()

startDate := time.Unix(startDateUnix/1000, 0).UTC().Truncate(24 * time.Hour)
endDate := time.Unix(endDateUnix/1000, 0).UTC().Truncate(24 * time.Hour)
today := time.Now().UTC().Truncate(24 * time.Hour)

var startingHeight = c.DataSource.Height()

if startDate != today {
duration := startDate.Sub(today)
minutes := duration.Minutes() + duration.Hours()*60
minutesPerBlock := c.Params.TargetTimePerBlock.Minutes() + c.Params.TargetTimePerBlock.Hours()*60
blockDiff := minutes / minutesPerBlock
blockDiff = math.Abs(blockDiff)
if startDate.Before(today) {
startingHeight = c.DataSource.Height() - int64(blockDiff)
} else {
startingHeight = c.DataSource.Height() + int64(blockDiff)
}
}

// accumulated staking reward
var stakePerc = ticketPoolValue / coinSupplyTmp
asr, ticketPrice, simulationTable := c.simulateStakingReward((endDate.Sub(startDate)).Hours()/24, startingBalance, true,
stakePerc, coinSupplyTmp, float64(startingHeight), ticketPriceInput, float64(c.DataSource.Height()))
writeJSON(w, struct {
Height int64 `json:"height"`
Reward float64 `json:"reward"`
TicketPrice float64 `json:"ticketPrice"`
SimulationTable []simulationRow `json:"simulation_table"`
}{
Height: startingHeight,
Reward: asr,
TicketPrice: ticketPrice,
SimulationTable: simulationTable,
}, m.GetIndentCtx(r))
}

func (c *appContext) getDecodedTx(w http.ResponseWriter, r *http.Request) {
// Look up any spending transactions for each output of this transaction
// when the client requests spends with the URL query ?spends=true.
Expand Down Expand Up @@ -2088,3 +2172,123 @@ func (c *appContext) getBlockHashCtx(r *http.Request) (string, error) {
}
return hash, nil
}

// Simulate ticket purchase and re-investment over a full year for a given
// starting amount of DCR and calculation parameters. Generate a TEXT table of
// the simulation results that can optionally be used for future expansion of
// dcrdata functionality.
func (c *appContext) simulateStakingReward(numberOfDays float64, startingDCRBalance float64, integerTicketQty bool,
currentStakePercent float64, actualCoinbase float64, startingBlockHeight float64,
actualTicketPrice float64, height float64) (stakingReward, ticketPrice float64, simulationTable []simulationRow) {

blocksPerDay := 86400 / c.Params.TargetTimePerBlock.Seconds()
numberOfBlocks := numberOfDays * blocksPerDay
ticketsPurchased := float64(0)
StakeRewardAtBlock := func(blocknum float64) float64 {
// Option 1: RPC Call

Subsidy, _ := c.nodeClient.GetBlockSubsidy(context.TODO(), int64(blocknum), 1)
return dcrutil.Amount(Subsidy.PoS).ToCoin()

// Option 2: Calculation
// epoch := math.Floor(blocknum / float64(exp.ChainParams.SubsidyReductionInterval))
// RewardProportionPerVote := float64(exp.ChainParams.StakeRewardProportion) / (10 * float64(exp.ChainParams.TicketsPerBlock))
// return RewardProportionPerVote * dcrutil.Amount(exp.ChainParams.BaseSubsidy).ToCoin() *
// math.Pow(float64(exp.ChainParams.MulSubsidy)/float64(exp.ChainParams.DivSubsidy), epoch)
}

MaxCoinSupplyAtBlock := func(blocknum float64) float64 {
// 4th order poly best fit curve to Decred mainnet emissions plot.
// Curve fit was done with 0 Y intercept and Pre-Mine added after.

return (-9e-19*math.Pow(blocknum, 4) +
7e-12*math.Pow(blocknum, 3) -
2e-05*math.Pow(blocknum, 2) +
29.757*blocknum + 76963 +
1680000) // Premine 1.68M
}

CoinAdjustmentFactor := actualCoinbase / MaxCoinSupplyAtBlock(startingBlockHeight)
var meanVotingBlock = txhelpers.CalcMeanVotingBlocks(c.Params)
TheoreticalTicketPrice := func(blocknum float64) float64 {
ProjectedCoinsCirculating := MaxCoinSupplyAtBlock(blocknum) * CoinAdjustmentFactor * currentStakePercent
TicketPoolSize := (float64(meanVotingBlock) + float64(c.Params.TicketMaturity) +
float64(c.Params.CoinbaseMaturity)) * float64(c.Params.TicketsPerBlock)
return ProjectedCoinsCirculating / TicketPoolSize
}
ticketPrice = TheoreticalTicketPrice(startingBlockHeight)
TicketAdjustmentFactor := actualTicketPrice / TheoreticalTicketPrice(height)
// Prepare for simulation
simblock := startingBlockHeight
var TicketPrice float64
DCRBalance := startingDCRBalance

simulationTable = append(simulationTable, simulationRow{
SimBlock: simblock,
SimDay: 0,
DCRBalance: DCRBalance,
TicketPrice: ticketPrice,
})

for simblock < (numberOfBlocks + startingBlockHeight) {
// Simulate a Purchase on simblock
TicketPrice = TheoreticalTicketPrice(simblock) * TicketAdjustmentFactor
if integerTicketQty {
// Use this to simulate integer qtys of tickets up to max funds
ticketsPurchased = math.Floor(DCRBalance / TicketPrice)
} else {
// Use this to simulate ALL funds used to buy tickets - even fractional tickets
// which is actually not possible
ticketsPurchased = (DCRBalance / TicketPrice)
}

simulationTable[len(simulationTable)-1].TicketPrice = TicketPrice
simulationTable[len(simulationTable)-1].TicketsPurchased = ticketsPurchased

DCRBalance -= (TicketPrice * ticketsPurchased)

// Move forward to average vote
simblock += (float64(c.Params.TicketMaturity) + float64(meanVotingBlock))

// Simulate return of funds
DCRBalance += (TicketPrice * ticketsPurchased)

// Simulate reward
DCRBalance += (StakeRewardAtBlock(simblock) * ticketsPurchased)

blocksPassed := simblock - simulationTable[len(simulationTable)-1].SimBlock
daysPassed := blocksPassed / blocksPerDay
day := simulationTable[len(simulationTable)-1].SimDay + int(daysPassed)
simulationTable = append(simulationTable, simulationRow{
SimBlock: simblock,
SimDay: day,
DCRBalance: DCRBalance,
Reward: (StakeRewardAtBlock(simblock) * ticketsPurchased),
ReturnedFund: (TicketPrice * ticketsPurchased),
TicketPrice: TheoreticalTicketPrice(simblock) * TicketAdjustmentFactor,
})

// Move forward to coinbase maturity
simblock += float64(c.Params.CoinbaseMaturity)
// Need to receive funds before we can use them again so add 1 block
simblock++
}

// Scale down to exactly numberOfDays days
SimulationReward := ((DCRBalance - startingDCRBalance) / startingDCRBalance) * 100
excessBlocks := (simblock - startingBlockHeight)
stakingReward = (numberOfBlocks / excessBlocks) * SimulationReward
overflow := startingDCRBalance * (SimulationReward - stakingReward) / 100
simulationTable[len(simulationTable)-1].DCRBalance -= overflow
simulationTable[len(simulationTable)-1].SimDay -= int(excessBlocks / blocksPerDay)
simulationTable[len(simulationTable)-1].Reward -= overflow
// remove nagative rewards from the table
for i := len(simulationTable) - 1; i > 0; i-- {
if simulationTable[i].Reward >= 0 {
break
}
simulationTable[i-1].Reward += simulationTable[i].Reward
simulationTable[i].Reward = 0
}
return
}
2 changes: 1 addition & 1 deletion cmd/dcrdata/internal/explorer/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func New(cfg *ExplorerConfig) *explorerUI {
"rawtx", "status", "parameters", "agenda", "agendas", "charts",
"sidechains", "disapproved", "ticketpool", "visualblocks", "statistics",
"windows", "timelisting", "addresstable", "proposals", "proposal",
"market", "insight_root", "attackcost", "treasury", "treasurytable", "verify_message"}
"market", "insight_root", "attackcost", "treasury", "treasurytable", "verify_message", "stakingreward"}

for _, name := range tmpls {
if err := exp.templates.addTemplate(name); err != nil {
Expand Down
39 changes: 39 additions & 0 deletions cmd/dcrdata/internal/explorer/explorerroutes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2779,6 +2779,45 @@ func (exp *explorerUI) AttackCost(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, str)
}

// StakeRewardCalcPage is the page handler for the "/stakingcalc" path.
func (exp *explorerUI) StakeRewardCalcPage(w http.ResponseWriter, r *http.Request) {
price := 24.42
if exp.xcBot != nil {
if rate := exp.xcBot.Conversion(1.0); rate != nil {
price = rate.Value
}
}

exp.pageData.RLock()

ticketReward := exp.pageData.HomeInfo.TicketReward
rewardPeriod := exp.pageData.HomeInfo.RewardPeriod

exp.pageData.RUnlock()

str, err := exp.templates.execTemplateToString("stakingreward", struct {
*CommonPageData
RewardPeriod string
TicketReward float64
DCRPrice float64
}{
CommonPageData: exp.commonData(r),
RewardPeriod: rewardPeriod,
TicketReward: ticketReward,
DCRPrice: price,
})

if err != nil {
log.Errorf("Template execute failure: %v", err)
exp.StatusPage(w, defaultErrorCode, defaultErrorMessage, "", ExpStatusError)
return
}

w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusOK)
io.WriteString(w, str)
}

type verifyMessageResult struct {
Address string
Signature string
Expand Down
1 change: 1 addition & 0 deletions cmd/dcrdata/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,7 @@ func _main(ctx context.Context) error {
r.With(explorer.MenuFormParser).Post("/set", explore.Home)
r.Get("/attack-cost", explore.AttackCost)
r.Get("/verify-message", explore.VerifyMessagePage)
r.Get("/stakingcalc", explore.StakeRewardCalcPage)
r.With(mw.Tollbooth(limiter)).Post("/verify-message", explore.VerifyMessageHandler)
})

Expand Down
Loading