Add network proportion to homepage and /vspinfo (and revoked proportion) (#264)

* Add network proportion to homepage and /vspinfo.

Proportion is calculated using the number of tickets  currently registered with the VSP, divided by the total size of the network ticket pool as reported by `getblockheader`.

The value will only ever be an estimate because:

- it's possible for a single ticket to be added to multiple VSPs.
- vspd does not distinguish between immature and live tickets, whereas `getblockheader` only reports live tickets.
- `getblockheader` is reporting the size of the ticket pool as of the previous block, not the current block.

* xaur suggestions

* Show missed ticket %, not just the raw number.
This commit is contained in:
Jamie Holdstock 2021-06-08 21:47:07 +08:00 committed by GitHub
parent 978b78e745
commit 9867f78385
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 70 additions and 56 deletions

View File

@ -54,7 +54,8 @@ when a VSP is closed will result in an error.
"voting":10, "voting":10,
"voted":25, "voted":25,
"revoked":3, "revoked":3,
"blockheight":623212 "blockheight":623212,
"estimatednetworkproportion":0.048478414
} }
``` ```

View File

@ -224,17 +224,6 @@ func (c *DcrdRPC) CanTicketVote(rawTx *dcrdtypes.TxRawResult, ticketHash string,
return live, nil return live, nil
} }
// GetBestBlockHeight uses getblockcount RPC to query the height of the best
// block known by the dcrd instance.
func (c *DcrdRPC) GetBestBlockHeight() (int64, error) {
var height int64
err := c.Call(c.ctx, "getblockcount", &height)
if err != nil {
return 0, err
}
return height, nil
}
// ParseBlockConnectedNotification extracts the block header from a // ParseBlockConnectedNotification extracts the block header from a
// blockconnected JSON-RPC notification. // blockconnected JSON-RPC notification.
func ParseBlockConnectedNotification(params json.RawMessage) (*wire.BlockHeader, error) { func ParseBlockConnectedNotification(params json.RawMessage) (*wire.BlockHeader, error) {

View File

@ -53,3 +53,7 @@ func indentJSON(input string) template.HTML {
func atomsToDCR(atoms int64) string { func atomsToDCR(atoms int64) string {
return dcrutil.Amount(atoms).String() return dcrutil.Amount(atoms).String()
} }
func float32ToPercent(input float32) string {
return fmt.Sprintf("%.2f%%", input*100)
}

View File

@ -21,18 +21,20 @@ import (
// vspStats is used to cache values which are commonly used by the API, so // vspStats is used to cache values which are commonly used by the API, so
// repeated web requests don't repeatedly trigger DB or RPC calls. // repeated web requests don't repeatedly trigger DB or RPC calls.
type vspStats struct { type vspStats struct {
PubKey string PubKey string
Voting int64 Voting int64
Voted int64 Voted int64
Revoked int64 Revoked int64
VSPFee float64 VSPFee float64
Network string Network string
UpdateTime string UpdateTime string
SupportEmail string SupportEmail string
VspClosed bool VspClosed bool
Debug bool Debug bool
Designation string Designation string
BlockHeight int64 BlockHeight uint32
NetworkProportion float32
RevokedProportion float32
} }
var statsMtx sync.RWMutex var statsMtx sync.RWMutex
@ -80,7 +82,7 @@ func updateVSPStats(ctx context.Context, db *database.VspDatabase,
return err return err
} }
blockHeight, err := dcrdClient.GetBestBlockHeight() bestBlock, err := dcrdClient.GetBestBlockHeader()
if err != nil { if err != nil {
return err return err
} }
@ -92,7 +94,9 @@ func updateVSPStats(ctx context.Context, db *database.VspDatabase,
stats.Voting = voting stats.Voting = voting
stats.Voted = voted stats.Voted = voted
stats.Revoked = revoked stats.Revoked = revoked
stats.BlockHeight = blockHeight stats.BlockHeight = bestBlock.Height
stats.NetworkProportion = float32(voting) / float32(bestBlock.PoolSize)
stats.RevokedProportion = float32(revoked) / float32(voted)
return nil return nil
} }

View File

@ -53,6 +53,11 @@ body {
color: #091440; color: #091440;
} }
.vsp-stats .stat-value .text-muted{
font-size: 18px;
color: #8997A5;
}
footer { footer {
flex-shrink: 0; flex-shrink: 0;
font-size: 0.8rem; font-size: 0.8rem;

View File

@ -14,7 +14,10 @@
<div class="col-6 col-sm-4 col-lg-2 py-3"> <div class="col-6 col-sm-4 col-lg-2 py-3">
<div class="stat-title">Revoked tickets</div> <div class="stat-title">Revoked tickets</div>
<div class="stat-value">{{ .Revoked }}</div> <div class="stat-value">
{{ .Revoked }}
<span class="text-muted">({{ float32ToPercent .RevokedProportion }})</span>
</div>
</div> </div>
<div class="col-6 col-sm-4 col-lg-2 py-3"> <div class="col-6 col-sm-4 col-lg-2 py-3">
@ -27,6 +30,11 @@
<div class="stat-value">{{ .Network }}</div> <div class="stat-value">{{ .Network }}</div>
</div> </div>
<div class="col-6 col-sm-4 col-lg-2 py-3">
<div class="stat-title">Network Proportion</div>
<div class="stat-value">{{ float32ToPercent .NetworkProportion }}</div>
</div>
</div> </div>
{{ end }} {{ end }}

View File

@ -5,17 +5,18 @@
package webapi package webapi
type vspInfoResponse struct { type vspInfoResponse struct {
APIVersions []int64 `json:"apiversions"` APIVersions []int64 `json:"apiversions"`
Timestamp int64 `json:"timestamp"` Timestamp int64 `json:"timestamp"`
PubKey []byte `json:"pubkey"` PubKey []byte `json:"pubkey"`
FeePercentage float64 `json:"feepercentage"` FeePercentage float64 `json:"feepercentage"`
VspClosed bool `json:"vspclosed"` VspClosed bool `json:"vspclosed"`
Network string `json:"network"` Network string `json:"network"`
VspdVersion string `json:"vspdversion"` VspdVersion string `json:"vspdversion"`
Voting int64 `json:"voting"` Voting int64 `json:"voting"`
Voted int64 `json:"voted"` Voted int64 `json:"voted"`
Revoked int64 `json:"revoked"` Revoked int64 `json:"revoked"`
BlockHeight int64 `json:"blockheight"` BlockHeight uint32 `json:"blockheight"`
NetworkProportion float32 `json:"estimatednetworkproportion"`
} }
type feeAddressRequest struct { type feeAddressRequest struct {

View File

@ -15,16 +15,17 @@ import (
func vspInfo(c *gin.Context) { func vspInfo(c *gin.Context) {
cachedStats := getVSPStats() cachedStats := getVSPStats()
sendJSONResponse(vspInfoResponse{ sendJSONResponse(vspInfoResponse{
APIVersions: []int64{3}, APIVersions: []int64{3},
Timestamp: time.Now().Unix(), Timestamp: time.Now().Unix(),
PubKey: signPubKey, PubKey: signPubKey,
FeePercentage: cfg.VSPFee, FeePercentage: cfg.VSPFee,
Network: cfg.NetParams.Name, Network: cfg.NetParams.Name,
VspClosed: cfg.VspClosed, VspClosed: cfg.VspClosed,
VspdVersion: version.String(), VspdVersion: version.String(),
Voting: cachedStats.Voting, Voting: cachedStats.Voting,
Voted: cachedStats.Voted, Voted: cachedStats.Voted,
Revoked: cachedStats.Revoked, Revoked: cachedStats.Revoked,
BlockHeight: cachedStats.BlockHeight, BlockHeight: cachedStats.BlockHeight,
NetworkProportion: cachedStats.NetworkProportion,
}, c) }, c)
} }

View File

@ -177,13 +177,14 @@ func router(debugMode bool, cookieSecret []byte, dcrd rpc.DcrdConnect, wallets r
// Add custom functions for use in templates. // Add custom functions for use in templates.
router.SetFuncMap(template.FuncMap{ router.SetFuncMap(template.FuncMap{
"txURL": txURL(cfg.BlockExplorerURL), "txURL": txURL(cfg.BlockExplorerURL),
"addressURL": addressURL(cfg.BlockExplorerURL), "addressURL": addressURL(cfg.BlockExplorerURL),
"blockURL": blockURL(cfg.BlockExplorerURL), "blockURL": blockURL(cfg.BlockExplorerURL),
"dateTime": dateTime, "dateTime": dateTime,
"stripWss": stripWss, "stripWss": stripWss,
"indentJSON": indentJSON, "indentJSON": indentJSON,
"atomsToDCR": atomsToDCR, "atomsToDCR": atomsToDCR,
"float32ToPercent": float32ToPercent,
}) })
router.LoadHTMLGlob("webapi/templates/*.html") router.LoadHTMLGlob("webapi/templates/*.html")