Skip to content
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
37 changes: 37 additions & 0 deletions db/el_accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,43 @@ func DeleteElAccount(id uint64, dbTx *sqlx.Tx) error {
return err
}

// GetElAccountsByAddresses retrieves multiple accounts by their addresses in a single query.
// Returns a map from address (as hex string) to account for efficient lookup.
func GetElAccountsByAddresses(addresses [][]byte) (map[string]*dbtypes.ElAccount, error) {
if len(addresses) == 0 {
return make(map[string]*dbtypes.ElAccount), nil
}

var sql strings.Builder
args := make([]any, len(addresses))

fmt.Fprint(&sql, "SELECT id, address, funder_id, funded, is_contract, last_nonce, last_block_uid FROM el_accounts WHERE address IN (")
for i, addr := range addresses {
if i > 0 {
fmt.Fprint(&sql, ", ")
}
fmt.Fprintf(&sql, "$%d", i+1)
args[i] = addr
}
fmt.Fprint(&sql, ")")

accounts := []*dbtypes.ElAccount{}
err := ReaderDb.Select(&accounts, sql.String(), args...)
if err != nil {
logger.Errorf("Error while fetching el accounts by addresses: %v", err)
return nil, err
}

// Build map from address to account
result := make(map[string]*dbtypes.ElAccount, len(accounts))
for _, account := range accounts {
// Use hex string of address as key for efficient lookup
key := fmt.Sprintf("%x", account.Address)
result[key] = account
}
return result, nil
}

// GetElAccountsByIDs retrieves multiple accounts by their IDs in a single query.
func GetElAccountsByIDs(ids []uint64) ([]*dbtypes.ElAccount, error) {
if len(ids) == 0 {
Expand Down
37 changes: 37 additions & 0 deletions db/el_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,43 @@ func DeleteElToken(id uint64, dbTx *sqlx.Tx) error {
return err
}

// GetElTokensByContracts retrieves multiple tokens by their contract addresses in a single query.
// Returns a map from contract address (as hex string) to token for efficient lookup.
func GetElTokensByContracts(contracts [][]byte) (map[string]*dbtypes.ElToken, error) {
if len(contracts) == 0 {
return make(map[string]*dbtypes.ElToken), nil
}

var sql strings.Builder
args := make([]any, len(contracts))

fmt.Fprint(&sql, "SELECT id, contract, token_type, name, symbol, decimals, flags, metadata_uri, name_synced FROM el_tokens WHERE contract IN (")
for i, contract := range contracts {
if i > 0 {
fmt.Fprint(&sql, ", ")
}
fmt.Fprintf(&sql, "$%d", i+1)
args[i] = contract
}
fmt.Fprint(&sql, ")")

tokens := []*dbtypes.ElToken{}
err := ReaderDb.Select(&tokens, sql.String(), args...)
if err != nil {
logger.Errorf("Error while fetching el tokens by contracts: %v", err)
return nil, err
}

// Build map from contract to token
result := make(map[string]*dbtypes.ElToken, len(tokens))
for _, token := range tokens {
// Use hex string of contract address as key for efficient lookup
key := fmt.Sprintf("%x", token.Contract)
result[key] = token
}
return result, nil
}

// GetElTokensByIDs retrieves multiple tokens by their IDs in a single query.
func GetElTokensByIDs(ids []uint64) ([]*dbtypes.ElToken, error) {
if len(ids) == 0 {
Expand Down
15 changes: 7 additions & 8 deletions handlers/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func Address(w http.ResponseWriter, r *http.Request) {
addressBytes, err := hex.DecodeString(addressHex)
if err != nil || len(addressBytes) != 20 {
data := InitPageData(w, r, "blockchain", "/address", "Address not found", notfoundTemplateFiles)
data.Data = "invalid"
w.Header().Set("Content-Type", "text/html")
handleTemplateError(w, r, "address.go", "Address", "invalidAddress", templates.GetTemplate(notfoundTemplateFiles...).ExecuteTemplate(w, "layout", data))
return
Expand Down Expand Up @@ -154,10 +155,9 @@ func AddressBalances(w http.ResponseWriter, r *http.Request) {
}

// Queue balance lookups (rate limited to 10min per account)
if account.ID > 0 {
if txIndexer := services.GlobalBeaconService.GetTxIndexer(); txIndexer != nil {
txIndexer.QueueAddressBalanceLookups(account.ID, account.Address)
}
// Even for unknown addresses (ID=0), we queue to check if they have balance
if txIndexer := services.GlobalBeaconService.GetTxIndexer(); txIndexer != nil {
txIndexer.QueueAddressBalanceLookups(account.ID, account.Address)
}

// Build response
Expand Down Expand Up @@ -270,10 +270,9 @@ func buildAddressPageData(account *dbtypes.ElAccount, tabView string, pageIdx, p
logrus.Debugf("address page called: %v (tab: %v)", account.ID, tabView)

// Queue balance lookups when page is viewed (rate limited to 10min per account)
if account.ID > 0 {
if txIndexer := services.GlobalBeaconService.GetTxIndexer(); txIndexer != nil {
txIndexer.QueueAddressBalanceLookups(account.ID, account.Address)
}
// Even for unknown addresses (ID=0), we queue to check if they have balance
if txIndexer := services.GlobalBeaconService.GetTxIndexer(); txIndexer != nil {
txIndexer.QueueAddressBalanceLookups(account.ID, account.Address)
}

chainState := services.GlobalBeaconService.GetChainState()
Expand Down
4 changes: 2 additions & 2 deletions handlers/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func buildBlocksPageData(firstSlot uint64, pageSize uint64, displayColumns uint6
displayMask |= 1 << (col - 1)
}
displayColumnsParam := ""
if displayMask != 0 {
if displayColumns != 0 {
displayColumnsParam = fmt.Sprintf("&d=0x%x", displayMask)
}

Expand Down Expand Up @@ -186,7 +186,7 @@ func buildBlocksPageData(firstSlot uint64, pageSize uint64, displayColumns uint6
// Populate UrlParams for page jump functionality
pageData.UrlParams = make(map[string]string)
pageData.UrlParams["c"] = fmt.Sprintf("%v", pageData.PageSize)
if displayMask != 0 {
if displayColumns != 0 {
pageData.UrlParams["d"] = fmt.Sprintf("0x%x", displayMask)
}
pageData.MaxSlot = uint64(maxSlot)
Expand Down
81 changes: 81 additions & 0 deletions handlers/slot.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,10 @@ func buildSlotPageData(ctx context.Context, blockSlot int64, blockRoot []byte) (
cacheTimeout = 10 * time.Second
}

// Get all blocks for this slot (used for multi-block display and proposer fallback)
slotBlocks, slotBlockProposers := getSlotBlocks(slot, blockRoot, blockData)
pageData.SlotBlocks = slotBlocks

if blockData == nil {
pageData.Status = uint16(models.SlotStatusMissed)
pageData.Proposer = math.MaxInt64
Expand All @@ -270,6 +274,10 @@ func buildSlotPageData(ctx context.Context, blockSlot int64, blockRoot []byte) (
if pageData.Proposer == math.MaxInt64 {
pageData.Proposer = db.GetSlotAssignment(uint64(slot))
}
// If proposer is still unknown, check if there's exactly one orphaned block and use its proposer
if pageData.Proposer == math.MaxInt64 && len(slotBlockProposers) == 1 {
pageData.Proposer = slotBlockProposers[0]
}
pageData.ProposerName = services.GlobalBeaconService.GetValidatorName(pageData.Proposer)
} else {
if blockData.Orphaned {
Expand Down Expand Up @@ -313,6 +321,79 @@ func buildSlotPageData(ctx context.Context, blockSlot int64, blockRoot []byte) (
return pageData, cacheTimeout
}

// getSlotBlocks retrieves all blocks for a given slot and builds the SlotBlocks slice
// for the multi-block display. Uses GetDbBlocksByFilter which handles both cache and database.
// Also returns a list of proposers from orphaned blocks (used as fallback when proposer is unknown).
func getSlotBlocks(slot phase0.Slot, currentBlockRoot []byte, currentBlockData *services.CombinedBlockResponse) ([]*models.SlotPageSlotBlock, []uint64) {
slotBlocks := make([]*models.SlotPageSlotBlock, 0)
orphanedProposers := make([]uint64, 0)
hasCanonicalOrMissed := false

// Get all blocks for the slot (from cache and database)
slotNum := uint64(slot)
dbBlocks := services.GlobalBeaconService.GetDbBlocksByFilter(&dbtypes.BlockFilter{
Slot: &slotNum,
WithOrphaned: 1, // include both canonical and orphaned
WithMissing: 1, // include missing slots
}, 0, 100, 0)

for _, dbBlock := range dbBlocks {
if dbBlock.Block == nil {
// This is a missed slot row (canonical proposer info without a block)
hasCanonicalOrMissed = true
slotBlocks = append(slotBlocks, &models.SlotPageSlotBlock{
BlockRoot: nil, // nil indicates missed
Status: uint16(models.SlotStatusMissed),
IsCurrent: currentBlockData == nil,
})
continue
}

var blockRoot phase0.Root
copy(blockRoot[:], dbBlock.Block.Root)

isCanonical := dbBlock.Block.Status == dbtypes.Canonical
if isCanonical {
hasCanonicalOrMissed = true
} else {
// Track orphaned block proposers for fallback
orphanedProposers = append(orphanedProposers, dbBlock.Block.Proposer)
}

isCurrent := false
if currentBlockData != nil && blockRoot == currentBlockData.Root {
isCurrent = true
} else if len(currentBlockRoot) == 32 && blockRoot == phase0.Root(currentBlockRoot) {
isCurrent = true
}

status := uint16(models.SlotStatusOrphaned)
if isCanonical {
status = uint16(models.SlotStatusFound)
}

slotBlocks = append(slotBlocks, &models.SlotPageSlotBlock{
BlockRoot: blockRoot[:],
Status: status,
IsCurrent: isCurrent,
})
}

// If no canonical or missed block was returned but there are orphaned blocks,
// add a "missed (canonical)" entry (fallback for edge cases)
if !hasCanonicalOrMissed && len(slotBlocks) > 0 {
missedBlock := &models.SlotPageSlotBlock{
BlockRoot: nil, // nil indicates missed
Status: uint16(models.SlotStatusMissed),
IsCurrent: currentBlockData == nil,
}
// Insert missed block at the beginning
slotBlocks = append([]*models.SlotPageSlotBlock{missedBlock}, slotBlocks...)
}

return slotBlocks, orphanedProposers
}

func getSlotPageBlockData(blockData *services.CombinedBlockResponse, epochStatsValues *beacon.EpochStatsValues, blockUid uint64) *models.SlotPageBlockData {
chainState := services.GlobalBeaconService.GetChainState()
specs := chainState.GetSpecs()
Expand Down
4 changes: 2 additions & 2 deletions handlers/slots.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func buildSlotsPageData(firstSlot uint64, pageSize uint64, displayColumns uint64
displayMask |= 1 << (col - 1)
}
displayColumnsParam := ""
if displayMask != 0 {
if displayColumns != 0 {
displayColumnsParam = fmt.Sprintf("&d=0x%x", displayMask)
}

Expand Down Expand Up @@ -191,7 +191,7 @@ func buildSlotsPageData(firstSlot uint64, pageSize uint64, displayColumns uint64
// Populate UrlParams for page jump functionality
pageData.UrlParams = make(map[string]string)
pageData.UrlParams["c"] = fmt.Sprintf("%v", pageData.PageSize)
if displayMask != 0 {
if displayColumns != 0 {
pageData.UrlParams["d"] = fmt.Sprintf("0x%x", displayMask)
}
pageData.MaxSlot = uint64(maxSlot)
Expand Down
Loading