diff --git a/README.md b/README.md
index 718c1978..2c2cdb1f 100644
--- a/README.md
+++ b/README.md
@@ -23,10 +23,10 @@ Things that might be worth adding at some time
* Validator Overview & Details\
The current validator set is actually already maintained in memory. So it should be easy to add pages for basic validator related stuff.
- * [ ] Page: Validators List (`/validators`)
- * [ ] Page: Validator Details (`/validator/{validator_index}`)
- * [ ] Rough overview with status (activated, slashed, ...) & current balance
- * [ ] Recent Blocks (from db)
+ * [x] Page: Validators List (`/validators`)
+ * [x] Page: Validator Details (`/validator/{validator_index}`)
+ * [x] Rough overview with status (activated, slashed, ...) & current balance
+ * [x] Recent Blocks (from db)
* [ ] Recent Attestations (from cache)
* Track Sync Committees
* [ ] Database: table sync_committees (Sync Committee index)
diff --git a/cmd/explorer/main.go b/cmd/explorer/main.go
index 025ff945..71d6ed90 100644
--- a/cmd/explorer/main.go
+++ b/cmd/explorer/main.go
@@ -63,6 +63,8 @@ func startFrontend() {
router.HandleFunc("/slot/{slotOrHash}", handlers.Slot).Methods("GET")
router.HandleFunc("/search", handlers.Search).Methods("GET")
router.HandleFunc("/search/{type}", handlers.SearchAhead).Methods("GET")
+ router.HandleFunc("/validators", handlers.Validators).Methods("GET")
+ router.HandleFunc("/validator/{idxOrPubKey}", handlers.Validator).Methods("GET")
if utils.Config.Frontend.Debug {
// serve files from local directory when debugging, instead of from go embed file
diff --git a/config/default.config.yml b/config/default.config.yml
index 53d31403..7502f3e4 100644
--- a/config/default.config.yml
+++ b/config/default.config.yml
@@ -3,7 +3,6 @@
chain:
name: "mainnet"
#genesisTimestamp: 1688126460
- #genesisValidatorsRoot: "0xbf3c3d4683a5a4d286cd2a5ef7a5c1702f649eee82cdc7e87e05030102d12ccf"
#configPath: "../ephemery/config.yaml"
#displayName: "Ephemery Iteration xy"
@@ -31,10 +30,10 @@ beaconapi:
# CL Client RPC
endpoint: "http://127.0.0.1:5052"
- # local cache for RPC calls
+ # local cache for page models
localCacheSize: 100 # 100MB
- # remote cache for RPC calls & page models
+ # remote cache for page models
redisCacheAddr: ""
redisCachePrefix: ""
diff --git a/db/db.go b/db/db.go
index d894aa7f..14050f9e 100644
--- a/db/db.go
+++ b/db/db.go
@@ -18,6 +18,7 @@ import (
"github.com/jackc/pgx/v4/pgxpool"
_ "github.com/jackc/pgx/v4/stdlib"
+ "github.com/mitchellh/mapstructure"
)
//go:embed migrations/*.sql
@@ -358,6 +359,73 @@ func GetBlocksWithGraffiti(graffiti string, firstSlot uint64, offset uint64, lim
return blocks
}
+func GetAssignedBlocks(proposer uint64, firstSlot uint64, offset uint64, limit uint32, withOrphaned bool) []*dbtypes.AssignedBlock {
+ blockAssignments := []*dbtypes.AssignedBlock{}
+ orphanedLimit := ""
+ if !withOrphaned {
+ orphanedLimit = "AND NOT orphaned"
+ }
+ var sql strings.Builder
+ fmt.Fprintf(&sql, `SELECT slot_assignments.slot, slot_assignments.proposer`)
+ blockFields := []string{
+ "root", "slot", "parent_root", "state_root", "orphaned", "proposer", "graffiti", "graffiti_text",
+ "attestation_count", "deposit_count", "exit_count", "withdraw_count", "withdraw_amount", "attester_slashing_count",
+ "proposer_slashing_count", "bls_change_count", "eth_transaction_count", "eth_block_number", "eth_block_hash", "sync_participation",
+ }
+ for _, blockField := range blockFields {
+ fmt.Fprintf(&sql, ", blocks.%v AS \"block.%v\"", blockField, blockField)
+ }
+ fmt.Fprintf(&sql, `
+ FROM slot_assignments
+ LEFT JOIN blocks ON blocks.slot = slot_assignments.slot
+ WHERE slot_assignments.proposer = $1 AND slot_assignments.slot < $2 `+orphanedLimit+`
+ ORDER BY slot_assignments.slot DESC
+ LIMIT $3 OFFSET $4
+ `)
+ rows, err := ReaderDb.Query(sql.String(), proposer, firstSlot, limit, offset)
+ if err != nil {
+ logger.Errorf("Error while fetching blocks: %v", err)
+ return nil
+ }
+
+ scanArgs := make([]interface{}, len(blockFields)+2)
+ for rows.Next() {
+ scanVals := make([]interface{}, len(blockFields)+2)
+ for i := range scanArgs {
+ scanArgs[i] = &scanVals[i]
+ }
+ err := rows.Scan(scanArgs...)
+ if err != nil {
+ logger.Errorf("Error while parsing block: %v", err)
+ continue
+ }
+
+ blockAssignment := dbtypes.AssignedBlock{}
+ blockAssignment.Slot = uint64(scanVals[0].(int64))
+ blockAssignment.Proposer = uint64(scanVals[1].(int64))
+
+ if scanVals[2] != nil {
+ blockValMap := map[string]interface{}{}
+ for idx, fName := range blockFields {
+ blockValMap[fName] = scanVals[idx+2]
+ }
+ var block dbtypes.Block
+ cfg := &mapstructure.DecoderConfig{
+ Metadata: nil,
+ Result: &block,
+ TagName: "db",
+ }
+ decoder, _ := mapstructure.NewDecoder(cfg)
+ decoder.Decode(blockValMap)
+ blockAssignment.Block = &block
+ }
+
+ blockAssignments = append(blockAssignments, &blockAssignment)
+ }
+
+ return blockAssignments
+}
+
func GetSlotAssignmentsForSlots(firstSlot uint64, lastSlot uint64) []*dbtypes.SlotAssignment {
assignments := []*dbtypes.SlotAssignment{}
err := ReaderDb.Select(&assignments, `
diff --git a/dbtypes/other.go b/dbtypes/other.go
new file mode 100644
index 00000000..277cd1c1
--- /dev/null
+++ b/dbtypes/other.go
@@ -0,0 +1,7 @@
+package dbtypes
+
+type AssignedBlock struct {
+ Slot uint64 `db:"slot"`
+ Proposer uint64 `db:"proposer"`
+ Block *Block `db:"block"`
+}
diff --git a/go.mod b/go.mod
index 42bce145..5a06227c 100644
--- a/go.mod
+++ b/go.mod
@@ -21,6 +21,8 @@ require (
gopkg.in/yaml.v3 v3.0.1
)
+require github.com/mitchellh/mapstructure v1.5.0 // indirect
+
require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
diff --git a/go.sum b/go.sum
index 51e7d51e..89918d67 100644
--- a/go.sum
+++ b/go.sum
@@ -118,6 +118,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
diff --git a/handlers/pageData.go b/handlers/pageData.go
index 0d5f6771..1eae4a05 100644
--- a/handlers/pageData.go
+++ b/handlers/pageData.go
@@ -103,6 +103,15 @@ func createMenuItems(active string, isMain bool) []types.MainMenuItem {
},
},
},
+ {
+ Links: []types.NavigationLink{
+ {
+ Label: "Validators",
+ Path: "/validators",
+ Icon: "fa-table",
+ },
+ },
+ },
},
},
}
diff --git a/handlers/validator.go b/handlers/validator.go
new file mode 100644
index 00000000..e6f2c199
--- /dev/null
+++ b/handlers/validator.go
@@ -0,0 +1,174 @@
+package handlers
+
+import (
+ "bytes"
+ "encoding/hex"
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gorilla/mux"
+ "github.com/sirupsen/logrus"
+
+ "github.com/pk910/light-beaconchain-explorer/rpctypes"
+ "github.com/pk910/light-beaconchain-explorer/services"
+ "github.com/pk910/light-beaconchain-explorer/templates"
+ "github.com/pk910/light-beaconchain-explorer/types/models"
+ "github.com/pk910/light-beaconchain-explorer/utils"
+)
+
+// Validator will return the main "validator" page using a go template
+func Validator(w http.ResponseWriter, r *http.Request) {
+ var validatorTemplateFiles = append(layoutTemplateFiles,
+ "validator/validator.html",
+ "validator/recentBlocks.html",
+ "_svg/timeline.html",
+ )
+ var notfoundTemplateFiles = append(layoutTemplateFiles,
+ "validator/notfound.html",
+ )
+
+ var pageTemplate = templates.GetTemplate(validatorTemplateFiles...)
+
+ w.Header().Set("Content-Type", "text/html")
+ data := InitPageData(w, r, "validators", "/validator", "Validator", validatorTemplateFiles)
+
+ validatorSetRsp := services.GlobalBeaconService.GetCachedValidatorSet()
+ var validator *rpctypes.ValidatorEntry
+ if validatorSetRsp != nil {
+ vars := mux.Vars(r)
+ idxOrPubKey := strings.Replace(vars["idxOrPubKey"], "0x", "", -1)
+ validatorPubKey, err := hex.DecodeString(idxOrPubKey)
+ if err != nil || len(validatorPubKey) != 48 {
+ // search by index^
+ validatorIndex, err := strconv.ParseUint(vars["idxOrPubKey"], 10, 64)
+ if err == nil && validatorIndex < uint64(len(validatorSetRsp.Data)) {
+ validator = &validatorSetRsp.Data[validatorIndex]
+ }
+ } else {
+ // search by pubkey
+ for _, val := range validatorSetRsp.Data {
+ if bytes.Equal(val.Validator.PubKey, validatorPubKey) {
+ validator = &val
+ break
+ }
+ }
+ }
+ }
+
+ if validator == nil {
+ data := InitPageData(w, r, "blockchain", "/validator", "Validator not found", notfoundTemplateFiles)
+ if handleTemplateError(w, r, "validator.go", "Validator", "", templates.GetTemplate(notfoundTemplateFiles...).ExecuteTemplate(w, "layout", data)) != nil {
+ return // an error has occurred and was processed
+ }
+ return
+ }
+
+ data.Data = getValidatorPageData(uint64(validator.Index))
+
+ if handleTemplateError(w, r, "validators.go", "Validators", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil {
+ return // an error has occurred and was processed
+ }
+}
+
+func getValidatorPageData(validatorIndex uint64) *models.ValidatorPageData {
+ pageData := &models.ValidatorPageData{}
+ pageCacheKey := fmt.Sprintf("validator:%v", validatorIndex)
+ if !utils.Config.Frontend.Debug && services.GlobalBeaconService.GetFrontendCache(pageCacheKey, pageData) == nil {
+ logrus.Printf("validator page served from cache: %v", validatorIndex)
+ return pageData
+ }
+ logrus.Printf("validator page called: %v", validatorIndex)
+
+ validatorSetRsp := services.GlobalBeaconService.GetCachedValidatorSet()
+ validator := validatorSetRsp.Data[validatorIndex]
+
+ pageData = &models.ValidatorPageData{
+ CurrentEpoch: uint64(utils.TimeToEpoch(time.Now())),
+ Index: uint64(validator.Index),
+ Name: services.GlobalBeaconService.GetValidatorName(uint64(validator.Index)),
+ PublicKey: validator.Validator.PubKey,
+ Balance: uint64(validator.Balance),
+ EffectiveBalance: uint64(validator.Validator.EffectiveBalance),
+ BeaconState: validator.Status,
+ WithdrawCredentials: validator.Validator.WithdrawalCredentials,
+ }
+ if strings.HasPrefix(validator.Status, "pending") {
+ pageData.State = "Pending"
+ } else if validator.Status == "active_ongoing" {
+ pageData.State = "Active"
+ pageData.IsActive = true
+ } else if validator.Status == "active_exiting" {
+ pageData.State = "Exiting"
+ pageData.IsActive = true
+ } else if validator.Status == "active_slashed" {
+ pageData.State = "Slashed"
+ pageData.IsActive = true
+ } else if validator.Status == "exited_unslashed" {
+ pageData.State = "Exited"
+ } else if validator.Status == "exited_slashed" {
+ pageData.State = "Slashed"
+ } else {
+ pageData.State = validator.Status
+ }
+
+ if pageData.IsActive {
+ // load activity map
+ activityMap, maxActivity := services.GlobalBeaconService.GetValidatorActivity()
+ pageData.UpcheckActivity = activityMap[uint64(validator.Index)]
+ pageData.UpcheckMaximum = uint8(maxActivity)
+ }
+
+ if validator.Validator.ActivationEligibilityEpoch < 18446744073709551615 {
+ pageData.ShowEligible = true
+ pageData.EligibleEpoch = uint64(validator.Validator.ActivationEligibilityEpoch)
+ pageData.EligibleTs = utils.EpochToTime(uint64(validator.Validator.ActivationEligibilityEpoch))
+ }
+ if validator.Validator.ActivationEpoch < 18446744073709551615 {
+ pageData.ShowActivation = true
+ pageData.ActivationEpoch = uint64(validator.Validator.ActivationEpoch)
+ pageData.ActivationTs = utils.EpochToTime(uint64(validator.Validator.ActivationEpoch))
+ }
+ if validator.Validator.ExitEpoch < 18446744073709551615 {
+ pageData.ShowExit = true
+ pageData.WasActive = true
+ pageData.ExitEpoch = uint64(validator.Validator.ExitEpoch)
+ pageData.ExitTs = utils.EpochToTime(uint64(validator.Validator.ExitEpoch))
+ }
+ if validator.Validator.WithdrawalCredentials[0] == 0x01 {
+ pageData.ShowWithdrawAddress = true
+ pageData.WithdrawAddress = validator.Validator.WithdrawalCredentials[12:]
+ }
+
+ // load latest blocks
+ pageData.RecentBlocks = make([]*models.ValidatorPageDataBlocks, 0)
+ blocksData := services.GlobalBeaconService.GetDbBlocksByProposer(validatorIndex, 0, 10, true, true)
+ for _, blockData := range blocksData {
+ blockStatus := 1
+ if blockData.Block == nil {
+ blockStatus = 0
+ } else if blockData.Block.Orphaned {
+ blockStatus = 2
+ }
+ blockEntry := models.ValidatorPageDataBlocks{
+ Epoch: utils.EpochOfSlot(blockData.Slot),
+ Slot: blockData.Slot,
+ Ts: utils.SlotToTime(blockData.Slot),
+ Status: uint64(blockStatus),
+ }
+ if blockData.Block != nil {
+ blockEntry.Graffiti = blockData.Block.Graffiti
+ blockEntry.EthBlock = blockData.Block.EthBlockNumber
+ blockEntry.BlockRoot = fmt.Sprintf("0x%x", blockData.Block.Root)
+ }
+ pageData.RecentBlocks = append(pageData.RecentBlocks, &blockEntry)
+ }
+ pageData.RecentBlockCount = uint64(len(pageData.RecentBlocks))
+
+ if pageCacheKey != "" {
+ services.GlobalBeaconService.SetFrontendCache(pageCacheKey, pageData, 10*time.Minute)
+ }
+ return pageData
+}
diff --git a/handlers/validators.go b/handlers/validators.go
new file mode 100644
index 00000000..628c19d7
--- /dev/null
+++ b/handlers/validators.go
@@ -0,0 +1,168 @@
+package handlers
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/pk910/light-beaconchain-explorer/services"
+ "github.com/pk910/light-beaconchain-explorer/templates"
+ "github.com/pk910/light-beaconchain-explorer/types/models"
+ "github.com/pk910/light-beaconchain-explorer/utils"
+ "github.com/sirupsen/logrus"
+)
+
+// Validators will return the main "validators" page using a go template
+func Validators(w http.ResponseWriter, r *http.Request) {
+ var validatorsTemplateFiles = append(layoutTemplateFiles,
+ "validators/validators.html",
+ "_svg/professor.html",
+ )
+
+ var pageTemplate = templates.GetTemplate(validatorsTemplateFiles...)
+
+ w.Header().Set("Content-Type", "text/html")
+ data := InitPageData(w, r, "validators", "/validators", "Validators", validatorsTemplateFiles)
+
+ urlArgs := r.URL.Query()
+ var firstIdx uint64 = 0
+ if urlArgs.Has("s") {
+ firstIdx, _ = strconv.ParseUint(urlArgs.Get("s"), 10, 64)
+ }
+ var pageSize uint64 = 50
+ if urlArgs.Has("c") {
+ pageSize, _ = strconv.ParseUint(urlArgs.Get("c"), 10, 64)
+ }
+ var stateFilter string
+ if urlArgs.Has("q") {
+ stateFilter = urlArgs.Get("q")
+ }
+ data.Data = getValidatorsPageData(firstIdx, pageSize, stateFilter)
+
+ if handleTemplateError(w, r, "validators.go", "Validators", "", pageTemplate.ExecuteTemplate(w, "layout", data)) != nil {
+ return // an error has occurred and was processed
+ }
+}
+
+func getValidatorsPageData(firstValIdx uint64, pageSize uint64, stateFilter string) *models.ValidatorsPageData {
+ pageData := &models.ValidatorsPageData{}
+ pageCacheKey := fmt.Sprintf("validators:%v:%v:%v", firstValIdx, pageSize, stateFilter)
+ if !utils.Config.Frontend.Debug && services.GlobalBeaconService.GetFrontendCache(pageCacheKey, pageData) == nil {
+ logrus.Printf("validators page served from cache: %v:%v", firstValIdx, pageSize)
+ return pageData
+ }
+ logrus.Printf("validators page called: %v:%v:%v", firstValIdx, pageSize, stateFilter)
+
+ // get latest validator set
+ validatorSetRsp := services.GlobalBeaconService.GetCachedValidatorSet()
+ if validatorSetRsp == nil {
+ return pageData
+ }
+ validatorSet := validatorSetRsp.Data
+ if stateFilter != "" {
+ // TODO: apply filter
+ }
+
+ totalValidatorCount := uint64(len(validatorSet))
+ if firstValIdx == 0 {
+ pageData.IsDefaultPage = true
+ } else if firstValIdx > totalValidatorCount {
+ firstValIdx = totalValidatorCount
+ }
+
+ if pageSize > 100 {
+ pageSize = 100
+ }
+
+ pagesBefore := firstValIdx / pageSize
+ if (firstValIdx % pageSize) > 0 {
+ pagesBefore++
+ }
+ pagesAfter := (totalValidatorCount - firstValIdx) / pageSize
+ if ((totalValidatorCount - firstValIdx) % pageSize) > 0 {
+ pagesAfter++
+ }
+ pageData.PageSize = pageSize
+ pageData.TotalPages = pagesBefore + pagesAfter
+ pageData.CurrentPageIndex = pagesBefore + 1
+ pageData.CurrentPageValIdx = firstValIdx
+ if pagesBefore > 0 {
+ pageData.PrevPageIndex = pageData.CurrentPageIndex - 1
+ pageData.PrevPageValIdx = pageData.CurrentPageValIdx - pageSize
+ }
+ if pagesAfter > 1 {
+ pageData.NextPageIndex = pageData.CurrentPageIndex + 1
+ pageData.NextPageValIdx = pageData.CurrentPageValIdx + pageSize
+ }
+ pageData.LastPageValIdx = totalValidatorCount - pageSize
+
+ // load activity map
+ activityMap, maxActivity := services.GlobalBeaconService.GetValidatorActivity()
+
+ // get validators
+ lastValIdx := firstValIdx + pageSize
+ if lastValIdx >= totalValidatorCount {
+ lastValIdx = totalValidatorCount
+ }
+ pageData.Validators = make([]*models.ValidatorsPageDataValidator, 0)
+
+ for _, validator := range validatorSet[firstValIdx:lastValIdx] {
+ validatorData := &models.ValidatorsPageDataValidator{
+ Index: uint64(validator.Index),
+ Name: services.GlobalBeaconService.GetValidatorName(uint64(validator.Index)),
+ PublicKey: validator.Validator.PubKey,
+ Balance: uint64(validator.Balance),
+ EffectiveBalance: uint64(validator.Validator.EffectiveBalance),
+ }
+ if strings.HasPrefix(validator.Status, "pending") {
+ validatorData.State = "Pending"
+ } else if validator.Status == "active_ongoing" {
+ validatorData.State = "Active"
+ validatorData.ShowUpcheck = true
+ } else if validator.Status == "active_exiting" {
+ validatorData.State = "Exiting"
+ validatorData.ShowUpcheck = true
+ } else if validator.Status == "active_slashed" {
+ validatorData.State = "Slashed"
+ validatorData.ShowUpcheck = true
+ } else if validator.Status == "exited_unslashed" {
+ validatorData.State = "Exited"
+ } else if validator.Status == "exited_slashed" {
+ validatorData.State = "Slashed"
+ } else {
+ validatorData.State = validator.Status
+ }
+
+ if validatorData.ShowUpcheck {
+ validatorData.UpcheckActivity = activityMap[uint64(validator.Index)]
+ validatorData.UpcheckMaximum = uint8(maxActivity)
+ }
+
+ if validator.Validator.ActivationEpoch < 18446744073709551615 {
+ validatorData.ShowActivation = true
+ validatorData.ActivationEpoch = uint64(validator.Validator.ActivationEpoch)
+ validatorData.ActivationTs = utils.EpochToTime(uint64(validator.Validator.ActivationEpoch))
+ }
+ if validator.Validator.ExitEpoch < 18446744073709551615 {
+ validatorData.ShowExit = true
+ validatorData.ExitEpoch = uint64(validator.Validator.ExitEpoch)
+ validatorData.ExitTs = utils.EpochToTime(uint64(validator.Validator.ExitEpoch))
+ }
+ if validator.Validator.WithdrawalCredentials[0] == 0x01 {
+ validatorData.ShowWithdrawAddress = true
+ validatorData.WithdrawAddress = validator.Validator.WithdrawalCredentials[12:]
+ }
+
+ pageData.Validators = append(pageData.Validators, validatorData)
+ }
+ pageData.ValidatorCount = uint64(len(pageData.Validators))
+ pageData.FirstValidator = firstValIdx
+ pageData.LastValidator = lastValIdx
+
+ if pageCacheKey != "" {
+ services.GlobalBeaconService.SetFrontendCache(pageCacheKey, pageData, 10*time.Minute)
+ }
+ return pageData
+}
diff --git a/indexer/indexer.go b/indexer/indexer.go
index 55085b06..1a37b6ac 100644
--- a/indexer/indexer.go
+++ b/indexer/indexer.go
@@ -185,7 +185,7 @@ func (indexer *Indexer) GetCachedValidatorSet() *rpctypes.StandardV1StateValidat
return indexer.state.headValidators
}
-func (indexer *Indexer) BuildLiveEpoch(epoch uint64) *dbtypes.Epoch {
+func (indexer *Indexer) GetEpochVotes(epoch uint64) *EpochVotes {
indexer.state.cacheMutex.RLock()
defer indexer.state.cacheMutex.RUnlock()
@@ -194,6 +194,10 @@ func (indexer *Indexer) BuildLiveEpoch(epoch uint64) *dbtypes.Epoch {
return nil
}
+ return indexer.getEpochVotes(epoch, epochStats)
+}
+
+func (indexer *Indexer) getEpochVotes(epoch uint64, epochStats *EpochStats) *EpochVotes {
var firstBlock *BlockInfo
firstSlot := epoch * utils.Config.Chain.Config.SlotsPerEpoch
lastSlot := firstSlot + (utils.Config.Chain.Config.SlotsPerEpoch) - 1
@@ -219,8 +223,18 @@ slotLoop:
} else {
targetRoot = firstBlock.Header.Data.Header.Message.ParentRoot
}
- epochVotes := aggregateEpochVotes(indexer.state.cachedBlocks, epoch, epochStats, targetRoot, false)
+ return aggregateEpochVotes(indexer.state.cachedBlocks, epoch, epochStats, targetRoot, false)
+}
+
+func (indexer *Indexer) BuildLiveEpoch(epoch uint64) *dbtypes.Epoch {
+ indexer.state.cacheMutex.RLock()
+ defer indexer.state.cacheMutex.RUnlock()
+ epochStats := indexer.state.epochStats[epoch]
+ if epochStats == nil {
+ return nil
+ }
+ epochVotes := indexer.getEpochVotes(epoch, epochStats)
return buildDbEpoch(epoch, indexer.state.cachedBlocks, epochStats, epochVotes, nil)
}
diff --git a/indexer/votes.go b/indexer/votes.go
index 8bf86002..cffeec29 100644
--- a/indexer/votes.go
+++ b/indexer/votes.go
@@ -18,6 +18,7 @@ type EpochVotes struct {
headVoteAmount uint64
totalVoteAmount uint64
}
+ ActivityMap map[uint64]bool
}
func aggregateEpochVotes(blockMap map[uint64][]*BlockInfo, epoch uint64, epochStats *EpochStats, targetRoot []byte, currentOnly bool) *EpochVotes {
@@ -28,7 +29,9 @@ func aggregateEpochVotes(blockMap map[uint64][]*BlockInfo, epoch uint64, epochSt
lastSlot += utils.Config.Chain.Config.SlotsPerEpoch
}
- votes := EpochVotes{}
+ votes := EpochVotes{
+ ActivityMap: map[uint64]bool{},
+ }
votedBitsets := make(map[string][]byte)
for slot := firstSlot; slot <= lastSlot; slot++ {
@@ -59,6 +62,7 @@ func aggregateEpochVotes(blockMap map[uint64][]*BlockInfo, epoch uint64, epochSt
}
if utils.BitAtVector(voteBitset, bitIdx) {
voteAmount += uint64(epochStats.Validators.ValidatorBalances[validatorIdx])
+ votes.ActivityMap[validatorIdx] = true
}
}
}
diff --git a/rpctypes/beaconapi.go b/rpctypes/beaconapi.go
index f12ccb38..e7becf53 100644
--- a/rpctypes/beaconapi.go
+++ b/rpctypes/beaconapi.go
@@ -79,12 +79,7 @@ type EpochAssignments struct {
}
type StandardV1StateValidatorsResponse struct {
- Data []struct {
- Index Uint64Str `json:"index"`
- Balance Uint64Str `json:"balance"`
- Status string `json:"status"`
- Validator Validator `json:"validator"`
- } `json:"data"`
+ Data []ValidatorEntry `json:"data"`
}
type StandardV1GenesisResponse struct {
diff --git a/rpctypes/beacontypes.go b/rpctypes/beacontypes.go
index a0e3fc26..3afaef47 100644
--- a/rpctypes/beacontypes.go
+++ b/rpctypes/beacontypes.go
@@ -165,6 +165,13 @@ type Validator struct {
WithdrawableEpoch Uint64Str `json:"withdrawable_epoch"`
}
+type ValidatorEntry struct {
+ Index Uint64Str `json:"index"`
+ Balance Uint64Str `json:"balance"`
+ Status string `json:"status"`
+ Validator Validator `json:"validator"`
+}
+
type BlobSidecar struct {
BlockRoot BytesHexStr `json:"block_root"`
Index Uint64Str `json:"index"`
diff --git a/services/beaconservice.go b/services/beaconservice.go
index b57cf369..9f9dcf0d 100644
--- a/services/beaconservice.go
+++ b/services/beaconservice.go
@@ -3,6 +3,7 @@ package services
import (
"fmt"
"strings"
+ "sync"
"time"
"github.com/pk910/light-beaconchain-explorer/cache"
@@ -19,6 +20,13 @@ type BeaconService struct {
frontendCache *cache.TieredCache
indexer *indexer.Indexer
validatorNames *ValidatorNames
+
+ validatorActivityMutex sync.Mutex
+ validatorActivityStats struct {
+ cacheEpoch uint64
+ epochLimit uint64
+ activity map[uint64]uint8
+ }
}
var GlobalBeaconService *BeaconService
@@ -484,3 +492,170 @@ func (bs *BeaconService) GetDbBlocksByGraffiti(graffiti string, pageIdx uint64,
return resBlocks
}
+
+func (bs *BeaconService) GetDbBlocksByProposer(proposer uint64, pageIdx uint64, pageSize uint32, withMissing bool, withOrphaned bool) []*dbtypes.AssignedBlock {
+ cachedMatches := make([]struct {
+ slot uint64
+ block *indexer.BlockInfo
+ }, 0)
+ idxMinSlot := bs.indexer.GetLowestCachedSlot()
+ idxHeadSlot := bs.indexer.GetHeadSlot()
+ if idxMinSlot >= 0 {
+ idxHeadEpoch := utils.EpochOfSlot(idxHeadSlot)
+ idxMinEpoch := utils.EpochOfSlot(uint64(idxMinSlot))
+ for epochIdx := int64(idxHeadEpoch); epochIdx >= int64(idxMinEpoch); epochIdx-- {
+ epoch := uint64(epochIdx)
+ epochStats := bs.indexer.GetCachedEpochStats(epoch)
+ if epochStats == nil || epochStats.Assignments == nil {
+ continue
+ }
+
+ for slot, assigned := range epochStats.Assignments.ProposerAssignments {
+ if assigned != proposer {
+ continue
+ }
+ blocks := bs.indexer.GetCachedBlocks(slot)
+ haveBlock := false
+ if blocks != nil {
+ for bidx := 0; bidx < len(blocks); bidx++ {
+ block := blocks[bidx]
+ if block.Orphaned && !withOrphaned {
+ continue
+ }
+ if uint64(block.Block.Data.Message.ProposerIndex) != proposer {
+ continue
+ }
+ cachedMatches = append(cachedMatches, struct {
+ slot uint64
+ block *indexer.BlockInfo
+ }{
+ slot: slot,
+ block: block,
+ })
+ haveBlock = true
+ }
+ }
+ if !haveBlock && withMissing {
+ cachedMatches = append(cachedMatches, struct {
+ slot uint64
+ block *indexer.BlockInfo
+ }{
+ slot: slot,
+ block: nil,
+ })
+ }
+ }
+
+ }
+ }
+
+ cachedMatchesLen := uint64(len(cachedMatches))
+ cachedPages := cachedMatchesLen / uint64(pageSize)
+ resBlocks := make([]*dbtypes.AssignedBlock, 0)
+ resIdx := 0
+
+ cachedStart := pageIdx * uint64(pageSize)
+ cachedEnd := cachedStart + uint64(pageSize)
+ if cachedEnd+1 < cachedMatchesLen {
+ cachedEnd++
+ }
+
+ if cachedPages > 0 && pageIdx < cachedPages {
+ for _, block := range cachedMatches[cachedStart:cachedEnd] {
+ assignedBlock := dbtypes.AssignedBlock{
+ Slot: block.slot,
+ Proposer: proposer,
+ }
+ if block.block != nil {
+ assignedBlock.Block = bs.indexer.BuildLiveBlock(block.block)
+ }
+ resBlocks = append(resBlocks, &assignedBlock)
+
+ resIdx++
+ }
+ } else if pageIdx == cachedPages {
+ start := pageIdx * uint64(pageSize)
+ for _, block := range cachedMatches[start:] {
+ assignedBlock := dbtypes.AssignedBlock{
+ Slot: block.slot,
+ Proposer: proposer,
+ }
+ if block.block != nil {
+ assignedBlock.Block = bs.indexer.BuildLiveBlock(block.block)
+ }
+ resBlocks = append(resBlocks, &assignedBlock)
+ resIdx++
+ }
+ }
+ if resIdx > int(pageSize) {
+ return resBlocks
+ }
+
+ // load from db
+ var dbMinSlot uint64
+ if idxMinSlot < 0 {
+ dbMinSlot = utils.TimeToSlot(uint64(time.Now().Unix()))
+ } else {
+ dbMinSlot = uint64(idxMinSlot)
+ }
+
+ dbPage := pageIdx - cachedPages
+ dbCacheOffset := uint64(pageSize) - (cachedMatchesLen % uint64(pageSize))
+ var dbBlocks []*dbtypes.AssignedBlock
+ if dbPage == 0 {
+ dbBlocks = db.GetAssignedBlocks(proposer, dbMinSlot, 0, uint32(dbCacheOffset)+1, withOrphaned)
+ } else {
+ dbBlocks = db.GetAssignedBlocks(proposer, dbMinSlot, (dbPage-1)*uint64(pageSize)+dbCacheOffset, pageSize+1, withOrphaned)
+ }
+ if dbBlocks != nil {
+ for _, dbBlock := range dbBlocks {
+ resBlocks = append(resBlocks, dbBlock)
+ }
+ }
+
+ return resBlocks
+}
+
+func (bs *BeaconService) GetValidatorActivity() (map[uint64]uint8, uint64) {
+ activityMap := map[uint64]uint8{}
+ epochLimit := uint64(3)
+
+ idxHeadSlot := bs.indexer.GetHeadSlot()
+ idxHeadEpoch := utils.EpochOfSlot(idxHeadSlot)
+ if idxHeadEpoch < 1 {
+ return activityMap, 0
+ }
+ idxHeadEpoch--
+ idxMinSlot := bs.indexer.GetLowestCachedSlot()
+ if idxMinSlot < 0 {
+ return activityMap, 0
+ }
+ idxMinEpoch := utils.EpochOfSlot(uint64(idxMinSlot))
+
+ activityEpoch := utils.EpochOfSlot(idxHeadSlot - 1)
+ bs.validatorActivityMutex.Lock()
+ defer bs.validatorActivityMutex.Unlock()
+ if bs.validatorActivityStats.activity != nil && bs.validatorActivityStats.cacheEpoch == activityEpoch {
+ return bs.validatorActivityStats.activity, bs.validatorActivityStats.epochLimit
+ }
+
+ actualEpochCount := idxHeadEpoch - idxMinEpoch + 1
+ if actualEpochCount > epochLimit {
+ idxMinEpoch = idxHeadEpoch - epochLimit + 1
+ } else if actualEpochCount < epochLimit {
+ epochLimit = actualEpochCount
+ }
+
+ for epochIdx := int64(idxHeadEpoch); epochIdx >= int64(idxMinEpoch); epochIdx-- {
+ epoch := uint64(epochIdx)
+ epochVotes := bs.indexer.GetEpochVotes(epoch)
+ for valIdx := range epochVotes.ActivityMap {
+ activityMap[valIdx]++
+ }
+ }
+
+ bs.validatorActivityStats.cacheEpoch = activityEpoch
+ bs.validatorActivityStats.epochLimit = epochLimit
+ bs.validatorActivityStats.activity = activityMap
+ return activityMap, epochLimit
+}
diff --git a/static/css/layout.css b/static/css/layout.css
index dfc999b1..0e14982f 100644
--- a/static/css/layout.css
+++ b/static/css/layout.css
@@ -267,3 +267,8 @@ span.validator-label {
border: hidden;
}
}
+
+.text-truncate {
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
diff --git a/static/css/validator.css b/static/css/validator.css
new file mode 100644
index 00000000..4fd570a8
--- /dev/null
+++ b/static/css/validator.css
@@ -0,0 +1,215 @@
+/* begin validator lifecycle */
+.validator__lifecycle-container {
+ position: relative;
+ display: flex;
+ align-items: center;
+ margin: 24px 20px 8px 20px;
+ font-size: .85rem;
+}
+.validator__lifecycle-content {
+ display: flex;
+ max-width: 600px;
+ width: 100%;
+}
+.validator__lifecycle-node-container {
+ position: relative;
+}
+.validator__lifecycle-node-header {
+ position: absolute;
+ top: -28px;
+ left: 50%;
+ transform: translateX(-50%);
+ opacity: 60%;
+}
+.validator__lifecycle-node::before {
+ content: '';
+ position: absolute;
+ opacity: 20%;
+ height: 8px;
+ border-right: solid 1px var(--bs-body-color, black);
+ top: -12px;
+}
+.validator__lifecycle-node {
+ width: 18px;
+ height: 18px;
+ position: relative;
+ left: 50%;
+ transform: translateX(-50%);
+ border-radius: 50%;
+ border: solid 4px var(--bs-tertiary-color, #00000033);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.validator__lifecycle-node, .validator__lifecycle-node > * {
+ box-sizing: content-box;
+}
+.validator__lifecycle-progress {
+ position: relative;
+ flex: 1 1 20px;
+}
+.validator__lifecycle-progress::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translateX(-50%);
+ border-bottom: solid 1px var(--bs-body-color, black);
+ width: 85%;
+ opacity: 20%;
+}
+.validator__lifecycle-progress-epoch {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ bottom: -10px;
+ color: black;
+ width: 85%;
+ font-size: 90%;
+}
+.validator__lifecycle-progress-epoch > div {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+}
+.validator__lifecycle-container .validator__lifecycle-progress::after {
+ content: '';
+ position: absolute;
+ top: 50%;
+}
+.validator__lifecycle-container .active.validator__lifecycle-progress::after {
+ position: absolute;
+ left: 7.5%;
+ border-bottom: solid 1px var(--bs-body-color, black);
+ width: 45%;
+}
+.validator__lifecycle-container .complete.validator__lifecycle-progress::after {
+ position: absolute;
+ border-bottom: solid 1px var(--bs-body-color, black);
+ left: 7.5%;
+ width: 85%;
+}
+.validator__lifecycle-container .active .validator__lifecycle-node-header,
+.validator__lifecycle-container .failed .validator__lifecycle-node-header,
+.validator__lifecycle-container .online .validator__lifecycle-node-header,
+.validator__lifecycle-container .offline .validator__lifecycle-node-header,
+.validator__lifecycle-container .done .validator__lifecycle-node-header {
+ opacity: 1;
+}
+.validator__lifecycle-container .done .validator__lifecycle-node,
+.validator__lifecycle-container .online.validator__lifecycle-active .validator__lifecycle-node {
+ border-color: var(--success, green);
+ background-color: var(--success, green);
+}
+.validator__lifecycle-container .failed .validator__lifecycle-node,
+.validator__lifecycle-container .offline.validator__lifecycle-active .validator__lifecycle-node {
+ border-color: var(--red);
+ background-color: var(--red);
+}
+.validator__lifecycle-container i {
+ visibility: hidden;
+ position: absolute;
+ color: white;
+}
+.validator__lifecycle-container .done.validator__lifecycle-deposited .deposit-success,
+.validator__lifecycle-container .failed.validator__lifecycle-deposited .deposit-fail,
+.validator__lifecycle-container .done .fa-check,
+.validator__lifecycle-container .done .fa-door-open,
+.validator__lifecycle-container .failed .fa-door-open,
+.validator__lifecycle-container .online.validator__lifecycle-active .online,
+.validator__lifecycle-container .offline.validator__lifecycle-active .offline {
+ visibility: visible;
+}
+.validator__lifecycle-container .spinner {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+}
+.validator__lifecycle-container .double-bounce1,
+.validator__lifecycle-container .double-bounce2 {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ background-color: #219653;
+ opacity: 0.6;
+ top: 0;
+ left: 0;
+ visibility: hidden;
+ -webkit-animation: sk-bounce 3.0s infinite ease-in-out;
+ animation: sk-bounce 3.0s infinite ease-in-out;
+}
+.validator__lifecycle-container .active .double-bounce1,
+.validator__lifecycle-container .active .double-bounce2 {
+ visibility: visible;
+}
+.validator__lifecycle-container .active.slashed .double-bounce1,
+.validator__lifecycle-container .active.slashed .double-bounce2 {
+ background-color: var(--red);
+}
+.validator__lifecycle-container .active .double-bounce2 {
+ -webkit-animation-delay: -1.5s;
+ animation-delay: -1.5s;
+}
+@-webkit-keyframes sk-bounce {
+ 0%,
+ 100% {
+ -webkit-transform: scale(0.0);
+ }
+
+ 50% {
+ -webkit-transform: scale(1.5);
+ }
+}
+@keyframes sk-bounce {
+ 0%,
+ 100% {
+ transform: scale(0.0);
+ -webkit-transform: scale(0.0);
+ }
+
+ 50% {
+ transform: scale(1.5);
+ -webkit-transform: scale(1.5);
+ }
+}
+@media screen and (min-width: 960px) {
+ .validator__lifecycle-container i {
+ margin-bottom: 0;
+ }
+ .validator__lifecycle-progress-epoch {
+ bottom: -2px;
+ }
+ .validator__lifecycle-node {
+ width: 24px;
+ height: 24px;
+ border-width: 5px;
+ }
+}
+/* end validator lifecycle */
+
+/* responsive tabs for mobile screens (shows icons when tab is not selected and text when selected */
+@media screen and (max-width: 1450px) {
+ .validator__lifecycle-container {
+ width: 80%;
+ }
+ .nav-tabs .tab-text {
+ display: none;
+ }
+ .nav-tabs .active .tab-text {
+ display: initial;
+ }
+ .nav-tabs .active .tab-icon {
+ display: none;
+ }
+ .nav-tabs .active i {
+ margin-right: .25rem;
+ }
+ .nav-tabs .tab-text + .badge {
+ display: none;
+ }
+}
+.validator-card {
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+}
diff --git a/templates/validator/notfound.html b/templates/validator/notfound.html
new file mode 100644
index 00000000..4f20bb53
--- /dev/null
+++ b/templates/validator/notfound.html
@@ -0,0 +1,27 @@
+{{ define "js" }}
+{{ end }}
+
+{{ define "css" }}
+{{ end }}
+
+{{ define "page" }}
+
+
+
+
Validator not found
+
+
+
+
+
+
Sorry but we could not find the validator you are looking for
+
+
+
+{{ end }}
diff --git a/templates/validator/recentAttestations.html b/templates/validator/recentAttestations.html
new file mode 100644
index 00000000..51fdcc17
--- /dev/null
+++ b/templates/validator/recentAttestations.html
@@ -0,0 +1,38 @@
+{{ define "recentAttestations" }}
+
+
+
+
+
+
+
+ Epoch |
+ Slot |
+ Block |
+ Status |
+ Time |
+ Distance |
+
+
+
+
+
+ |
+
+
+ {{ template "timeline_svg" }}
+
+ |
+ |
+
+
+
+
+
+
+
+{{ end }}
diff --git a/templates/validator/recentBlocks.html b/templates/validator/recentBlocks.html
new file mode 100644
index 00000000..c63f09f7
--- /dev/null
+++ b/templates/validator/recentBlocks.html
@@ -0,0 +1,67 @@
+{{ define "recentBlocks" }}
+
+
+
+
+
+
+
+ Epoch |
+ Slot |
+ Block |
+ Status |
+ Time |
+ Graffiti |
+
+
+ {{ if gt .RecentBlockCount 0 }}
+
+ {{ range $i, $block := .RecentBlocks }}
+
+ {{ formatAddCommas $block.Epoch }} |
+ {{ if eq .Status 2 }}
+ {{ formatAddCommas $block.Slot }} |
+ {{ else }}
+ {{ formatAddCommas $block.Slot }} |
+ {{ end }}
+ {{ ethBlockLink $block.EthBlock }} |
+
+ {{ if eq $block.Slot 0 }}
+ Genesis
+ {{ else if eq .Status 0 }}
+ Missed
+ {{ else if eq .Status 1 }}
+ Proposed
+ {{ else if eq .Status 2 }}
+ Missed (Orphaned)
+ {{ else }}
+ Unknown
+ {{ end }}
+ |
+ {{ formatRecentTimeShort $block.Ts }} |
+ {{ formatGraffiti $block.Graffiti }} |
+
+ {{ end }}
+
+ {{ else }}
+
+
+ |
+
+
+ {{ template "timeline_svg" }}
+
+ |
+ |
+
+
+ {{ end }}
+
+
+
+
+{{ end }}
diff --git a/templates/validator/validator.html b/templates/validator/validator.html
new file mode 100644
index 00000000..04aae938
--- /dev/null
+++ b/templates/validator/validator.html
@@ -0,0 +1,170 @@
+{{ define "page" }}
+
+
+
Validator {{ formatValidatorWithIndex .Index .Name }}
+
+
+
+
+
+
+
+
+ Status:
+
+
+
+
+
+
+
+
+ {{ if .ShowEligible }}
+
+ {{ end }}
+
+
+
+
+
+ {{ if .ShowActivation }}
+
+ {{ end }}
+
+
+
+
+
+ {{ if .ShowExit }}
+
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
Index:
+
+ {{ formatValidatorWithIndex .Index .Name }}
+
+
+
+
+
Public Key:
+
+ 0x{{ printf "%x" .PublicKey }}
+
+
+
+
+
Status:
+
+ {{ .BeaconState }}
+
+
+
+
Balance:
+
+ {{ formatEthFromGwei .Balance }}
+
+
+
+
+
Effective Balance:
+
+ {{ formatEthAddCommasFromGwei .EffectiveBalance }} ETH
+
+
+
+
W/Credentials:
+
+ 0x{{ printf "%x" .WithdrawCredentials }}
+
+
+
+ {{ if .ShowWithdrawAddress }}
+
+
W/Address:
+
+ {{ ethAddressLink .WithdrawAddress }}
+
+
+ {{ end }}
+
+
+
+
+
+
+ {{ template "recentBlocks" . }}
+
+
+
+{{ end }}
+{{ define "js" }}
+{{ end }}
+{{ define "css" }}
+
+{{ end }}
\ No newline at end of file
diff --git a/templates/validators/validators.html b/templates/validators/validators.html
new file mode 100644
index 00000000..6033c6f4
--- /dev/null
+++ b/templates/validators/validators.html
@@ -0,0 +1,155 @@
+{{ define "page" }}
+
+
+
Validators Overview
+
+
+
+
+
+
+
+
+
+
+
+ Index |
+ Public Key |
+ Balance |
+ State |
+ Activation |
+ Exit |
+ W/address |
+
+
+ {{ if gt .ValidatorCount 0 }}
+
+ {{ range $i, $validator := .Validators }}
+
+ {{ formatValidatorWithIndex $validator.Index $validator.Name }} |
+ 0x{{ printf "%x" $validator.PublicKey }} |
+ {{ formatEthFromGwei $validator.Balance }} ({{ formatEthAddCommasFromGwei $validator.EffectiveBalance }} ETH) |
+
+ {{- $validator.State -}}
+ {{- if $validator.ShowUpcheck -}}
+ {{- if eq $validator.UpcheckActivity $validator.UpcheckMaximum }}
+
+ {{- else if gt $validator.UpcheckActivity 0 }}
+
+ {{- else }}
+
+ {{- end -}}
+ {{- end -}}
+ |
+
+ {{- if $validator.ShowActivation -}}
+ {{ formatRecentTimeShort $validator.ActivationTs }}
+ (Epoch {{ formatAddCommas $validator.ActivationEpoch }})
+ {{- else -}}
+ -
+ {{- end -}}
+ |
+
+ {{- if $validator.ShowExit -}}
+ {{ formatRecentTimeShort $validator.ExitTs }}
+ (Epoch {{ formatAddCommas $validator.ExitEpoch }})
+ {{- else -}}
+ -
+ {{- end -}}
+ |
+ {{ ethAddressLink .WithdrawAddress }} |
+
+ {{ end }}
+
+ {{ else }}
+
+
+ |
+
+
+ {{ template "professor_svg" }}
+
+ |
+ |
+
+
+ {{ end }}
+
+
+ {{ if gt .TotalPages 1 }}
+
+ {{ end }}
+
+
+
+
+{{ end }}
+{{ define "js" }}
+{{ end }}
+{{ define "css" }}
+{{ end }}
\ No newline at end of file
diff --git a/types/models/validator.go b/types/models/validator.go
new file mode 100644
index 00000000..b85955f5
--- /dev/null
+++ b/types/models/validator.go
@@ -0,0 +1,46 @@
+package models
+
+import (
+ "time"
+)
+
+// ValidatorPageData is a struct to hold info for the validator page
+type ValidatorPageData struct {
+ CurrentEpoch uint64 `json:"current_epoch"`
+ Index uint64 `json:"index"`
+ Name string `json:"name"`
+ PublicKey []byte `json:"pubkey"`
+ Balance uint64 `json:"balance"`
+ EffectiveBalance uint64 `json:"eff_balance"`
+ State string `json:"state"`
+ BeaconState string `json:"beacon_state"`
+ ShowEligible bool `json:"show_eligible"`
+ EligibleTs time.Time `json:"eligible_ts"`
+ EligibleEpoch uint64 `json:"eligible_epoch"`
+ ShowActivation bool `json:"show_activation"`
+ ActivationTs time.Time `json:"activation_ts"`
+ ActivationEpoch uint64 `json:"activation_epoch"`
+ IsActive bool `json:"is_active"`
+ WasActive bool `json:"was_active"`
+ UpcheckActivity uint8 `json:"upcheck_act"`
+ UpcheckMaximum uint8 `json:"upcheck_max"`
+ ShowExit bool `json:"show_exit"`
+ ExitTs time.Time `json:"exit_ts"`
+ ExitEpoch uint64 `json:"exit_epoch"`
+ WithdrawCredentials []byte `json:"withdraw_credentials"`
+ ShowWithdrawAddress bool `json:"show_withdraw_address"`
+ WithdrawAddress []byte `json:"withdraw_address"`
+
+ RecentBlocks []*ValidatorPageDataBlocks `json:"recent_blocks"`
+ RecentBlockCount uint64 `json:"recent_block_count"`
+}
+
+type ValidatorPageDataBlocks struct {
+ Epoch uint64 `json:"epoch"`
+ Slot uint64 `json:"slot"`
+ EthBlock uint64 `json:"eth_block"`
+ Ts time.Time `json:"ts"`
+ Status uint64 `json:"status"`
+ BlockRoot string `json:"block_root"`
+ Graffiti []byte `json:"graffiti"`
+}
diff --git a/types/models/validators.go b/types/models/validators.go
new file mode 100644
index 00000000..2c5d807d
--- /dev/null
+++ b/types/models/validators.go
@@ -0,0 +1,45 @@
+package models
+
+import (
+ "time"
+)
+
+// ValidatorsPageData is a struct to hold info for the validators page
+type ValidatorsPageData struct {
+ Validators []*ValidatorsPageDataValidator `json:"validators"`
+ ValidatorCount uint64 `json:"validator_count"`
+ FirstValidator uint64 `json:"first_validx"`
+ LastValidator uint64 `json:"last_validx"`
+ StateFilter string `json:"state_filter"`
+
+ IsDefaultPage bool `json:"default_page"`
+ TotalPages uint64 `json:"total_pages"`
+ PageSize uint64 `json:"page_size"`
+ CurrentPageIndex uint64 `json:"page_index"`
+ CurrentPageValIdx uint64 `json:"page_validx"`
+ PrevPageIndex uint64 `json:"prev_page_index"`
+ PrevPageValIdx uint64 `json:"prev_page_validx"`
+ NextPageIndex uint64 `json:"next_page_index"`
+ NextPageValIdx uint64 `json:"next_page_validx"`
+ LastPageValIdx uint64 `json:"last_page_validx"`
+}
+
+type ValidatorsPageDataValidator struct {
+ Index uint64 `json:"index"`
+ Name string `json:"name"`
+ PublicKey []byte `json:"pubkey"`
+ Balance uint64 `json:"balance"`
+ EffectiveBalance uint64 `json:"eff_balance"`
+ State string `json:"state"`
+ ShowUpcheck bool `json:"show_upcheck"`
+ UpcheckActivity uint8 `json:"upcheck_act"`
+ UpcheckMaximum uint8 `json:"upcheck_max"`
+ ShowActivation bool `json:"show_activation"`
+ ActivationTs time.Time `json:"activation_ts"`
+ ActivationEpoch uint64 `json:"activation_epoch"`
+ ShowExit bool `json:"show_exit"`
+ ExitTs time.Time `json:"exit_ts"`
+ ExitEpoch uint64 `json:"exit_epoch"`
+ ShowWithdrawAddress bool `json:"show_withdraw_address"`
+ WithdrawAddress []byte `json:"withdraw_address"`
+}
diff --git a/utils/format.go b/utils/format.go
index 35f91e9d..798e5125 100644
--- a/utils/format.go
+++ b/utils/format.go
@@ -300,9 +300,16 @@ func FormatSlashedValidator(index uint64, name string) template.HTML {
func formatValidator(index uint64, name string, icon string) template.HTML {
if name != "" {
- return template.HTML(fmt.Sprintf(" %v", index, icon, html.EscapeString(name)))
+ return template.HTML(fmt.Sprintf(" %v", index, icon, index, html.EscapeString(name)))
}
- return template.HTML(fmt.Sprintf(" %v", icon, index))
+ return template.HTML(fmt.Sprintf(" %v", icon, index, index))
+}
+
+func FormatValidatorWithIndex(index uint64, name string) template.HTML {
+ if name != "" {
+ return template.HTML(fmt.Sprintf("%v (%v)", html.EscapeString(name), index))
+ }
+ return template.HTML(fmt.Sprintf("%v", index))
}
func FormatRecentTimeShort(ts time.Time) template.HTML {
diff --git a/utils/templateFucs.go b/utils/templateFucs.go
index 0f1465cf..75501c00 100644
--- a/utils/templateFucs.go
+++ b/utils/templateFucs.go
@@ -49,6 +49,7 @@ func GetTemplateFuncs() template.FuncMap {
"ethBlockHashLink": FormatEthBlockHashLink,
"ethAddressLink": FormatEthAddressLink,
"formatValidator": FormatValidator,
+ "formatValidatorWithIndex": FormatValidatorWithIndex,
"formatSlashedValidator": FormatSlashedValidator,
"formatRecentTimeShort": FormatRecentTimeShort,
"formatGraffiti": FormatGraffiti,