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: '

Validators by Withdrawal Credentials

', + suggestion: function (data) { + return `
${data.count}: 0x${data.withdrawalcredentials}
` + }, + }, + }, { limit: 5, name: "graffiti", @@ -581,6 +607,8 @@ $(document).ready(function () { window.location = "/address/" + sug.address } else if (sug.eth1_address !== undefined) { window.location = "/validators/deposits?q=" + sug.eth1_address + } else if (sug.withdrawalcredentials !== undefined) { + window.location = "/validators/deposits?q=" + sug.withdrawalcredentials } else if (sug.graffiti !== undefined) { // sug.graffiti is html-escaped to prevent xss, we need to unescape it var el = document.createElement("textarea") diff --git a/utils/format.go b/utils/format.go index ed312356ff..5b6ddbf46d 100644 --- a/utils/format.go +++ b/utils/format.go @@ -31,6 +31,7 @@ import ( ) const CalculatingHint = `Calculating…` +const BeginningOfSetWithdrawalCredentials = "010000000000000000000000" func FormatMessageToHtml(message string) template.HTML { message = fmt.Sprint(strings.Replace(message, "Error: ", "", 1)) @@ -743,7 +744,7 @@ func FormatWithdawalCredentials(hash []byte, addCopyButton bool) template.HTML { } func FormatAddressToWithdrawalCredentials(address []byte, addCopyButton bool) template.HTML { - credentials, err := hex.DecodeString("010000000000000000000000") + credentials, err := hex.DecodeString(BeginningOfSetWithdrawalCredentials) if err != nil { return "INVALID CREDENTIALS" } diff --git a/utils/utils.go b/utils/utils.go index 480f084481..745a49ca2f 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -880,7 +880,7 @@ func IsApiRequest(r *http.Request) bool { var eth1AddressRE = regexp.MustCompile("^(0x)?[0-9a-fA-F]{40}$") var withdrawalCredentialsRE = regexp.MustCompile("^(0x)?00[0-9a-fA-F]{62}$") -var withdrawalCredentialsAddressRE = regexp.MustCompile("^(0x)?010000000000000000000000[0-9a-fA-F]{40}$") +var withdrawalCredentialsAddressRE = regexp.MustCompile("^(0x)?" + BeginningOfSetWithdrawalCredentials + "[0-9a-fA-F]{40}$") var eth1TxRE = regexp.MustCompile("^(0x)?[0-9a-fA-F]{64}$") var zeroHashRE = regexp.MustCompile("^(0x)?0+$") var hashRE = regexp.MustCompile("^(0x)?[0-9a-fA-F]{96}$")