diff --git a/src/beacon-api-client.js b/src/beacon-api-client.js index b7d1d51..bb187d7 100644 --- a/src/beacon-api-client.js +++ b/src/beacon-api-client.js @@ -1,4 +1,5 @@ import axios from 'axios'; +import qs from 'qs'; class BeaconAPIClient { constructor(beaconAPIs) { @@ -9,13 +10,20 @@ class BeaconAPIClient { } async getValidators(stateId, pubKeys) { - const { data } = await this.queryEndpoint(`/eth/v1/beacon/states/${stateId}/validators?id=${pubKeys.join(',')}`) + const options = { + params: { id: pubKeys }, + paramsSerializer: (params) => { + return qs.stringify(params, { arrayFormat: 'repeat' }) + } + }; + + const { data } = await this.queryEndpoint(`/eth/v1/beacon/states/${stateId}/validators`, options) return data; } - async queryEndpoint(url) { + async queryEndpoint(url, options = {}) { try { - const data = await this.http.get(url) + const data = await this.http.get(url, options) return data } catch (err) { console.log(`Request to ${url} failed with error ${err.message}`) diff --git a/src/utils.js b/src/utils.js index e32ab34..9a71af0 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1 +1,23 @@ -export const validatorShortName = (pubkey) => pubkey.slice(2,9); +export const validatorShortName = (pubkey) => pubkey.slice(2, 9); + +const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) + +export async function withRetry(fn, options = {}) { + const { maxAttempts = 5, interval = 3000 } = options; + let attempts = 0; + + while (attempts < maxAttempts) { + try { + return await fn(); + } catch (err) { + attempts++; + console.warn(`${attempts} of ${maxAttempts} attempts failed with error: ${err}`); + + if (attempts == maxAttempts) { + throw new Error(`Failed after ${attempts} attempts: ${err}`); + } + + await sleep(interval); + } + } +} \ No newline at end of file diff --git a/src/validator-polling-service.js b/src/validator-polling-service.js index 3f02d49..e89cef5 100644 --- a/src/validator-polling-service.js +++ b/src/validator-polling-service.js @@ -3,7 +3,7 @@ const SECONDS_PER_SLOT = 12; const SLOTS_PER_EPOCH = 32; const SECONDS_PER_EPOCH = SECONDS_PER_SLOT * SLOTS_PER_EPOCH; -import { validatorShortName } from './utils.js'; +import { validatorShortName, withRetry } from './utils.js'; class ValidatorPollingService { #pollingIntervalSeconds; @@ -31,7 +31,7 @@ class ValidatorPollingService { if (!allValid) throw new Error(`Failed to add validator key(s)`) this.#validatorPubKeys = this.#validatorPubKeys.concat(newPubKeys); - + console.log(`Validator(s) added: ${newPubKeys.map(k => validatorShortName(k)).join(',')}`) } @@ -67,13 +67,17 @@ class ValidatorPollingService { this.#genesisTime = await this.#beaconApiClient.getGenesisTime(); } - const currentSlot = Math.floor((new Date().getTime() / 1000 - this.#genesisTime) / SECONDS_PER_SLOT); - const previousEpochSlot = currentSlot - SLOTS_PER_EPOCH; + const queryValidatorStates = () => { + const currentSlot = Math.floor((new Date().getTime() / 1000 - this.#genesisTime) / SECONDS_PER_SLOT) - 1; + const previousEpochSlot = currentSlot - SLOTS_PER_EPOCH; + + return Promise.all([ + this.#beaconApiClient.getValidators(currentSlot, this.#validatorPubKeys), + this.#beaconApiClient.getValidators(previousEpochSlot, this.#validatorPubKeys), + ]); + }; - const [currentEpochData, previousEpochData] = await Promise.all([ - this.#beaconApiClient.getValidators(currentSlot, this.#validatorPubKeys), - this.#beaconApiClient.getValidators(previousEpochSlot, this.#validatorPubKeys), - ]); + const [currentEpochData, previousEpochData] = await withRetry(queryValidatorStates, { interval: SECONDS_PER_EPOCH }); const validatorStates = mergeValidatorData(currentEpochData.data, previousEpochData.data) this.#listeners.forEach(listener => listener(validatorStates));