diff --git a/handlers/api.go b/handlers/api.go index 6e6a84a9b3..832525c5c0 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -2097,7 +2097,7 @@ func ApiValidatorBlsChange(w http.ResponseWriter, r *http.Request) { Address: fmt.Sprintf("0x%x", d.Address), Signature: fmt.Sprintf("0x%x", d.Signature), WithdrawalCredentialsOld: fmt.Sprintf("0x%x", d.WithdrawalCredentialsOld), - WithdrawalCredentialsNew: fmt.Sprintf("0x010000000000000000000000%x", d.Address), + WithdrawalCredentialsNew: fmt.Sprintf("0x"+utils.BeginningOfSetWithdrawalCredentials+"%x", d.Address), }) } diff --git a/handlers/search.go b/handlers/search.go index 823b1731fa..439d144e31 100644 --- a/handlers/search.go +++ b/handlers/search.go @@ -39,11 +39,12 @@ func Search(w http.ResponseWriter, r *http.Request) { var ensData *types.EnsDomainResponse if utils.IsValidEnsDomain(search) { ensData, _ = GetEnsDomain(search) - } search = strings.Replace(search, "0x", "", -1) if ensData != nil && len(ensData.Address) > 0 { http.Redirect(w, r, "/address/"+ensData.Domain, http.StatusMovedPermanently) + } else if utils.IsValidWithdrawalCredentials(search) { + http.Redirect(w, r, "/validators/deposits?q="+search, http.StatusMovedPermanently) } else if utils.IsValidEth1Tx(search) { http.Redirect(w, r, "/tx/"+search, http.StatusMovedPermanently) } else if len(search) == 96 { @@ -206,7 +207,6 @@ func SearchAhead(w http.ResponseWriter, r *http.Request) { err = fmt.Errorf("error searching for eth1AddressHash: %v", err) } case "indexed_validators": - // find all validators that have a publickey or index like the search-query result = &types.SearchAheadValidatorsResult{} indexNumeric, errParse := strconv.ParseInt(search, 10, 32) @@ -251,7 +251,7 @@ func SearchAhead(w http.ResponseWriter, r *http.Request) { if !searchLikeRE.MatchString(lowerStrippedSearch) { break } - // find validators per eth1-address (limit result by N addresses and M validators per address) + // find validators per eth1-address result = &[]struct { Eth1Address string `db:"from_address_text" json:"eth1_address"` Count uint64 `db:"count" json:"count"` @@ -267,7 +267,46 @@ func SearchAhead(w http.ResponseWriter, r *http.Request) { WHERE from_address_text LIKE $1 || '%' ) a GROUP BY from_address_text`, lowerStrippedSearch) - + case "count_indexed_validators_by_withdrawal_credential": + var ensData *types.EnsDomainResponse + if utils.IsValidEnsDomain(search) { + ensData, _ = GetEnsDomain(search) + if len(ensData.Address) > 0 { + lowerStrippedSearch = strings.ToLower(strings.Replace(ensData.Address, "0x", "", -1)) + } + } + if len(lowerStrippedSearch) == 40 { + // when the user gives an address (that validators might withdraw to) we transform the address into credentials + lowerStrippedSearch = utils.BeginningOfSetWithdrawalCredentials + lowerStrippedSearch + } + if !utils.IsValidWithdrawalCredentials(lowerStrippedSearch) { + break + } + decodedCredential, decodeErr := hex.DecodeString(lowerStrippedSearch) + if decodeErr != nil { + break + } + // find validators per withdrawal credential + dbFinding := []struct { + DecodedCredential []byte `db:"withdrawalcredentials"` + Count uint64 `db:"count"` + }{} + err = db.ReaderDb.Select(&dbFinding, ` + SELECT withdrawalcredentials, COUNT(*) FROM validators + WHERE withdrawalcredentials = $1 + GROUP BY withdrawalcredentials`, decodedCredential) + if err == nil { + res := make([]struct { + EncodedCredential string `json:"withdrawalcredentials"` + Count uint64 `json:"count"` + }, + len(dbFinding)) + for i := range dbFinding { + res[i].EncodedCredential = fmt.Sprintf("%x", dbFinding[i].DecodedCredential) + res[i].Count = dbFinding[i].Count + } + result = &res + } case "indexed_validators_by_graffiti": // find validators per graffiti (limit result by N graffities and M validators per graffiti) res := []struct { @@ -353,7 +392,7 @@ func SearchAhead(w http.ResponseWriter, r *http.Request) { } } -// search can ether be a valid ETH address or an ENS name mapping to one +// search can either be a valid ETH address or an ENS name mapping to one func FindValidatorIndicesByEth1Address(search string) (types.SearchValidatorsByEth1Result, error) { search = strings.ToLower(strings.Replace(ReplaceEnsNameWithAddress(search), "0x", "", -1)) if !utils.IsValidEth1Address(search) { diff --git a/static/js/layout.js b/static/js/layout.js index 4fe01a84d2..9503bd376c 100644 --- a/static/js/layout.js +++ b/static/js/layout.js @@ -223,7 +223,7 @@ $(document).ready(function () { // set maxParallelRequests to number of datasets queried in each search // make sure this is set in every one bloodhound object - let requestNum = 10 + let requestNum = 11 var timeWait = 0 // used to overwrite Bloodhounds "transport._get" function which handles the rateLimitWait parameter @@ -386,6 +386,20 @@ $(document).ready(function () { }) bhValidatorsByAddress.remote.transport._get = debounce(bhValidatorsByAddress.remote.transport, bhValidatorsByAddress.remote.transport._get) + var bhValidatorsByWithdrawalCredential = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.whitespace, + queryTokenizer: Bloodhound.tokenizers.whitespace, + identify: function (obj) { + return obj.withdrawalcredentials + }, + remote: { + url: "/search/count_indexed_validators_by_withdrawal_credential/%QUERY", + wildcard: "%QUERY", + maxPendingRequests: requestNum, + }, + }) + bhValidatorsByWithdrawalCredential.remote.transport._get = debounce(bhValidatorsByWithdrawalCredential.remote.transport, bhValidatorsByWithdrawalCredential.remote.transport._get) + var bhPubkey = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.whitespace, queryTokenizer: Bloodhound.tokenizers.whitespace, @@ -527,6 +541,18 @@ $(document).ready(function () { }, }, }, + { + limit: 5, + name: "validators-by-withdrawal-credential", + source: bhValidatorsByWithdrawalCredential, + display: "withdrawalcredentials", + templates: { + header: '