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

multi: implement stake reward calculator #1949

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
92 changes: 36 additions & 56 deletions cmd/dcrdata/internal/explorer/explorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,8 @@ 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", "stake_reward"}

for _, name := range tmpls {
if err := exp.templates.addTemplate(name); err != nil {
Expand Down Expand Up @@ -569,9 +570,9 @@ func (exp *explorerUI) Store(blockData *blockdata.BlockData, msgBlock *wire.MsgB

// Simulate the annual staking rate.
go func(height int64, sdiff float64, supply int64) {
ASR, _ := exp.simulateASR(1000, false, stakePerc,
ASR := exp.simulateStakeReturn(1000, false, stakePerc,
dcrutil.Amount(supply).ToCoin(),
float64(height), sdiff)
float64(height), sdiff, 365 /* for a year */)
p.Lock()
p.HomeInfo.ASR = ASR
p.Unlock()
Expand Down Expand Up @@ -673,32 +674,30 @@ func (exp *explorerUI) addRoutes() {
exp.Mux.Get("/stats", redirect("statistics"))
}

// 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 (exp *explorerUI) simulateASR(StartingDCRBalance float64, IntegerTicketQty bool,
CurrentStakePercent float64, ActualCoinbase float64, CurrentBlockNum float64,
ActualTicketPrice float64) (ASR float64, ReturnTable string) {
// simulateStakeReturn simulates ticket purchase and re-investment over the
// specified durationInDays for a given starting amount of DCR and calculation
// parameters.
func (exp *explorerUI) simulateStakeReturn(startingDCRBalance float64, integerTicketQty bool,
currentStakePercent float64, actualCoinbase float64, currentBlockNum float64,
actualTicketPrice float64, durationInDays float64) float64 {

// Calculations are only useful on mainnet. Short circuit calculations if
// on any other version of chain params.
if exp.ChainParams.Name != "mainnet" {
return 0, ""
return 0
}

BlocksPerDay := 86400 / exp.ChainParams.TargetTimePerBlock.Seconds()
BlocksPerYear := 365 * BlocksPerDay
TicketsPurchased := float64(0)
blocksPerDay := 86400 / exp.ChainParams.TargetTimePerBlock.Seconds()
totalBlocksInDuration := durationInDays * blocksPerDay

votesPerBlock := exp.ChainParams.VotesPerBlock()

StakeRewardAtBlock := func(blocknum float64) float64 {
Subsidy := exp.dataSource.BlockSubsidy(int64(blocknum), votesPerBlock)
return dcrutil.Amount(Subsidy.PoS / int64(votesPerBlock)).ToCoin()
stakeRewardAtBlock := func(blocknum float64) float64 {
subsidy := exp.dataSource.BlockSubsidy(int64(blocknum), votesPerBlock)
return dcrutil.Amount(subsidy.PoS / int64(votesPerBlock)).ToCoin()
}

MaxCoinSupplyAtBlock := func(blocknum float64) float64 {
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.

Expand All @@ -709,73 +708,54 @@ func (exp *explorerUI) simulateASR(StartingDCRBalance float64, IntegerTicketQty
1680000) // Premine 1.68M
}

CoinAdjustmentFactor := ActualCoinbase / MaxCoinSupplyAtBlock(CurrentBlockNum)
coinAdjustmentFactor := actualCoinbase / maxCoinSupplyAtBlock(currentBlockNum)

TheoreticalTicketPrice := func(blocknum float64) float64 {
ProjectedCoinsCirculating := MaxCoinSupplyAtBlock(blocknum) * CoinAdjustmentFactor * CurrentStakePercent
TicketPoolSize := (float64(exp.MeanVotingBlocks) + float64(exp.ChainParams.TicketMaturity) +
theoreticalTicketPrice := func(blocknum float64) float64 {
projectedCoinsCirculating := maxCoinSupplyAtBlock(blocknum) * coinAdjustmentFactor * currentStakePercent
ticketPoolSize := (float64(exp.MeanVotingBlocks) + float64(exp.ChainParams.TicketMaturity) +
float64(exp.ChainParams.CoinbaseMaturity)) * float64(exp.ChainParams.TicketsPerBlock)
return ProjectedCoinsCirculating / TicketPoolSize
return projectedCoinsCirculating / ticketPoolSize
}
TicketAdjustmentFactor := ActualTicketPrice / TheoreticalTicketPrice(CurrentBlockNum)
ticketAdjustmentFactor := actualTicketPrice / theoreticalTicketPrice(currentBlockNum)

// Prepare for simulation
simblock := CurrentBlockNum
TicketPrice := ActualTicketPrice
DCRBalance := StartingDCRBalance
simblock := currentBlockNum
dcrBalance := startingDCRBalance

ReturnTable += "\n\nBLOCKNUM DCR TICKETS TKT_PRICE TKT_REWRD ACTION\n"
ReturnTable += fmt.Sprintf("%8d %9.2f %8.1f %9.2f %9.2f INIT\n",
int64(simblock), DCRBalance, TicketsPurchased,
TicketPrice, StakeRewardAtBlock(simblock))

for simblock < (BlocksPerYear + CurrentBlockNum) {
for simblock < (totalBlocksInDuration + currentBlockNum) {
// Simulate a Purchase on simblock
TicketPrice = TheoreticalTicketPrice(simblock) * TicketAdjustmentFactor
var ticketsPurchased float64
ticketPrice := theoreticalTicketPrice(simblock) * ticketAdjustmentFactor

if IntegerTicketQty {
if integerTicketQty {
// Use this to simulate integer qtys of tickets up to max funds
TicketsPurchased = math.Floor(DCRBalance / TicketPrice)
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)
ticketsPurchased = (dcrBalance / ticketPrice)
}

DCRBalance -= (TicketPrice * TicketsPurchased)
ReturnTable += fmt.Sprintf("%8d %9.2f %8.1f %9.2f %9.2f BUY\n",
int64(simblock), DCRBalance, TicketsPurchased,
TicketPrice, StakeRewardAtBlock(simblock))
dcrBalance -= (ticketPrice * ticketsPurchased)

// Move forward to average vote
simblock += (float64(exp.ChainParams.TicketMaturity) + float64(exp.MeanVotingBlocks))
ReturnTable += fmt.Sprintf("%8d %9.2f %8.1f %9.2f %9.2f VOTE\n",
int64(simblock), DCRBalance, TicketsPurchased,
(TheoreticalTicketPrice(simblock) * TicketAdjustmentFactor), StakeRewardAtBlock(simblock))

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

// Simulate reward
DCRBalance += (StakeRewardAtBlock(simblock) * TicketsPurchased)
TicketsPurchased = 0
dcrBalance += (stakeRewardAtBlock(simblock) * ticketsPurchased)

// Move forward to coinbase maturity
simblock += float64(exp.ChainParams.CoinbaseMaturity)

ReturnTable += fmt.Sprintf("%8d %9.2f %8.1f %9.2f %9.2f REWARD\n",
int64(simblock), DCRBalance, TicketsPurchased,
(TheoreticalTicketPrice(simblock) * TicketAdjustmentFactor), StakeRewardAtBlock(simblock))

// Need to receive funds before we can use them again so add 1 block
simblock++
}

// Scale down to exactly 365 days
SimulationReward := ((DCRBalance - StartingDCRBalance) / StartingDCRBalance) * 100
ASR = (BlocksPerYear / (simblock - CurrentBlockNum)) * SimulationReward
ReturnTable += fmt.Sprintf("ASR over 365 Days is %.2f.\n", ASR)
return
simulationReward := ((dcrBalance - startingDCRBalance) / startingDCRBalance) * 100
return (totalBlocksInDuration / (simblock - currentBlockNum)) * simulationReward
}

func (exp *explorerUI) watchExchanges() {
Expand Down
150 changes: 150 additions & 0 deletions cmd/dcrdata/internal/explorer/explorerroutes.go
Original file line number Diff line number Diff line change
Expand Up @@ -2854,3 +2854,153 @@ func (exp *explorerUI) VerifyMessageHandler(w http.ResponseWriter, r *http.Reque
}
displayPage("", true)
}

type stakeReward struct {
Reward float64
RewardInDcr float64
RewardDurationInDays float64
TotalTicketsCost float64
Amount string
StartDate string
EndDate string
Error string
}

// StakeReward is the page handler for the "GET /stake-reward" path.
func (exp *explorerUI) StakeReward(w http.ResponseWriter, r *http.Request) {
voteReward := exp.pageData.HomeInfo.NBlockSubsidy.PoS / int64(exp.ChainParams.VotesPerBlock())
str, err := exp.templates.exec("stake_reward", struct {
*CommonPageData
VoteReward float64
CurrentTicketPrice float64
TicketReward float64
MinimumRewardPeriod string
ExchangeRate *types.Conversion
StakeReward *stakeReward
}{
VoteReward: toFloat64Amount(voteReward),
CurrentTicketPrice: exp.pageData.HomeInfo.StakeDiff,
TicketReward: exp.pageData.HomeInfo.TicketReward,
MinimumRewardPeriod: exp.pageData.HomeInfo.RewardPeriod,
ExchangeRate: exp.pageData.HomeInfo.ExchangeRate,
CommonPageData: exp.commonData(r),
})
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)
}

// CalculateStakeReward is the handler for "POST /stake-reward" path.
func (exp *explorerUI) CalculateStakeReward(w http.ResponseWriter, r *http.Request) {
startDateStr := r.PostFormValue("startDate")
endDateStr := r.PostFormValue("endDate")
amountStr := r.PostFormValue("amount")

var reward, rewardInDcr, totalTicketCost float64
var durationInDays float64
var err error
exchangeRate := exp.pageData.HomeInfo.ExchangeRate
currentTicketPrice := exp.pageData.HomeInfo.StakeDiff
homeInfo := exp.pageData.HomeInfo
voteReward := exp.pageData.HomeInfo.NBlockSubsidy.PoS / int64(exp.ChainParams.VotesPerBlock())

displayPage := func(errMsg string) {
str, err := exp.templates.exec("stake_reward", struct {
*CommonPageData
VoteReward float64
CurrentTicketPrice float64
TicketReward float64
MinimumRewardPeriod string
ExchangeRate *types.Conversion
StakeReward *stakeReward
}{
CommonPageData: exp.commonData(r),
VoteReward: toFloat64Amount(voteReward),
CurrentTicketPrice: currentTicketPrice,
TicketReward: homeInfo.TicketReward,
MinimumRewardPeriod: homeInfo.RewardPeriod,
ExchangeRate: exchangeRate,
StakeReward: &stakeReward{
Reward: reward,
Amount: amountStr,
RewardDurationInDays: durationInDays,
TotalTicketsCost: totalTicketCost,
StartDate: startDateStr,
RewardInDcr: rewardInDcr,
EndDate: endDateStr,
Error: errMsg,
},
})

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)
}

startDate, err := time.Parse("2006-01-02", startDateStr)
if err != nil {
displayPage("invalid start date")
return
}

now := time.Now()
if startDate.Before(now) {
displayPage("staking start date must be in the future")
return
}

endDate, err := time.Parse("2006-01-02", endDateStr)
if err != nil {
displayPage("invalid end date")
return
}

if endDate.Before(now) {
displayPage("staking end date must be in the future")
return
}

if startDate.After(endDate) {
displayPage("staking start date cannot be before specified end date")
return
}

// Parse and ensure amount provided for tickets is sane.
amount, err := strconv.ParseInt(amountStr, 10, 64)
if err != nil {
displayPage(err.Error())
return
}

amountInDCR := float64(amount) / exchangeRate.Value
durationInDays = endDate.Sub(startDate).Hours() / 24
minimumRewardDurationInDays := ((float64(exp.ChainParams.TicketMaturity) +
float64(exp.MeanVotingBlocks) +
float64(exp.ChainParams.CoinbaseMaturity)) * exp.ChainParams.TargetTimePerBlock.Hours()) / 24

if durationInDays < minimumRewardDurationInDays {
displayPage(fmt.Sprintf("minimum stake reward duration is %.2f days", minimumRewardDurationInDays))
return
}

reward = exp.simulateStakeReturn(amountInDCR, false, homeInfo.PoolInfo.Percentage/100,
dcrutil.Amount(homeInfo.CoinSupply).ToCoin(), float64(exp.pageData.BlockInfo.Height),
currentTicketPrice, durationInDays)

rewardInDcr = amountInDCR * reward / 100
totalTicketCost = math.Floor(amountInDCR/currentTicketPrice) * currentTicketPrice

displayPage("")
}
11 changes: 8 additions & 3 deletions cmd/dcrdata/internal/explorer/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,10 @@ func formattedDuration(duration time.Duration, str *periodMap) string {
return i(durationsec) + pl(str.s, durationsec)
}

func toFloat64Amount(intAmount int64) float64 {
return dcrutil.Amount(intAmount).ToCoin()
}

func makeTemplateFuncMap(params *chaincfg.Params) template.FuncMap {
netTheme := "theme-" + strings.ToLower(netName(params))

Expand Down Expand Up @@ -363,6 +367,9 @@ func makeTemplateFuncMap(params *chaincfg.Params) template.FuncMap {
"divideFloat": func(n, d float64) float64 {
return n / d
},
"float64Multiply": func(x, y float64) float64 {
return x * y
},
"multiply": func(a, b int64) int64 {
return a * b
},
Expand Down Expand Up @@ -404,9 +411,7 @@ func makeTemplateFuncMap(params *chaincfg.Params) template.FuncMap {
"amountAsDecimalParts": func(v int64, useCommas bool) []string {
return float64Formatting(dcrutil.Amount(v).ToCoin(), 8, useCommas)
},
"toFloat64Amount": func(intAmount int64) float64 {
return dcrutil.Amount(intAmount).ToCoin()
},
"toFloat64Amount": toFloat64Amount,
"dcrPerKbToAtomsPerByte": func(amt dcrutil.Amount) int64 {
return int64(math.Round(float64(amt) / 1e3))
},
Expand Down
2 changes: 2 additions & 0 deletions cmd/dcrdata/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,8 @@ func _main(ctx context.Context) error {
r.Get("/attack-cost", explore.AttackCost)
r.Get("/verify-message", explore.VerifyMessagePage)
r.With(mw.Tollbooth(limiter)).Post("/verify-message", explore.VerifyMessageHandler)
r.Get("/stake-reward", explore.StakeReward)
r.With(mw.Tollbooth(limiter)).Post("/stake-reward", explore.CalculateStakeReward)
})

// Configure a page for the bare "/insight" path. This mounts the static
Expand Down
3 changes: 2 additions & 1 deletion cmd/dcrdata/views/extras.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
<span class="patty"></span>
<span class="patty"></span>
<span class="patty short"></span>
<div id="menu">
<div id="menu" style="max-height: 90vh; overflow-x: hidden;">
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have noticed the menu cuts a lot of the menu items out of view when the current view height is less than its full height. This will ensure dcrdata users can scroll the menu because we "could" add more items(maybe) but will not be limited anymore.

<a class="menu-item" data-keynav-skip href="/" title="Home">Home</a>
<a class="menu-item" data-keynav-skip href="/blocks" title="Decred blocks">Blocks</a>
<a class="menu-item" data-keynav-skip href="/mempool" title="Decred mempool">Mempool</a>
Expand All @@ -112,6 +112,7 @@
<a class="menu-item" data-keynav-skip href="/treasury" title="Decred Treasury">Treasury</a>
<a class="menu-item" data-keynav-skip href="/decodetx" title="Decode or send a raw transaction">Decode/Broadcast Tx</a>
<a class="menu-item" data-keynav-skip href="/verify-message" title="Verify Message">Verify Message</a>
<a class="menu-item" data-keynav-skip href="/stake-reward" title="Calculate Stake Reward">Calculate Stake Reward</a>
{{- if eq .NetName "Mainnet"}}
<a class="menu-item" data-keynav-skip href="{{.Links.Testnet}}" title="Home">Switch To Testnet</a>
{{- else}}
Expand Down
3 changes: 1 addition & 2 deletions cmd/dcrdata/views/home.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@
<span class="h4" data-homepage-target="mpRegTotal">{{threeSigFigs .Mempool.LikelyMineable.RegularTotal}}</span>
<span class="h6">DCR</span>
</div>
{{- /* TODO: pe-SM, pe-MD, ETC CAN GO */ -}}
<div class="text-end pe-3 pe-sm-3 pe-md-3 pe-lg-3 tx-bar tx-ticket d-inline-block">
<div class="text-end pe-3 tx-bar tx-ticket d-inline-block">
<span data-homepage-target="mpTicketCount" class="h4">{{.Mempool.NumTickets}}</span>
<span class="h6"> tickets</span>
<br>
Expand Down
Loading