use crate::*;
use alloc::vec::Vec;
use frame_election_provider_support::{BoundedSupportsOf, ElectionProvider, PageIndex};
use frame_support::{
pallet_prelude::*,
traits::{Defensive, DefensiveMax, DefensiveSaturating, OnUnbalanced, TryCollect},
};
use sp_runtime::{Perbill, Percent, Saturating};
use sp_staking::{
currency_to_vote::CurrencyToVote, Exposure, Page, PagedExposureMetadata, SessionIndex,
};
pub struct Eras<T: Config>(core::marker::PhantomData<T>);
impl<T: Config> Eras<T> {
pub(crate) fn prune_era(era: EraIndex) {
crate::log!(debug, "Pruning era {:?}", era);
let mut cursor = <ErasValidatorPrefs<T>>::clear_prefix(era, u32::MAX, None);
debug_assert!(cursor.maybe_cursor.is_none());
cursor = <ErasClaimedRewards<T>>::clear_prefix(era, u32::MAX, None);
debug_assert!(cursor.maybe_cursor.is_none());
cursor = <ErasStakersPaged<T>>::clear_prefix((era,), u32::MAX, None);
debug_assert!(cursor.maybe_cursor.is_none());
cursor = <ErasStakersOverview<T>>::clear_prefix(era, u32::MAX, None);
debug_assert!(cursor.maybe_cursor.is_none());
<ErasValidatorReward<T>>::remove(era);
<ErasRewardPoints<T>>::remove(era);
<ErasTotalStake<T>>::remove(era);
}
pub(crate) fn set_validator_prefs(era: EraIndex, stash: &T::AccountId, prefs: ValidatorPrefs) {
debug_assert_eq!(era, Rotator::<T>::planning_era(), "we only set prefs for planning era");
<ErasValidatorPrefs<T>>::insert(era, stash, prefs);
}
pub(crate) fn get_validator_prefs(era: EraIndex, stash: &T::AccountId) -> ValidatorPrefs {
<ErasValidatorPrefs<T>>::get(era, stash)
}
pub(crate) fn get_validator_commission(era: EraIndex, stash: &T::AccountId) -> Perbill {
Self::get_validator_prefs(era, stash).commission
}
pub(crate) fn pending_rewards(era: EraIndex, validator: &T::AccountId) -> bool {
<ErasStakersOverview<T>>::get(&era, validator)
.map(|overview| {
ErasClaimedRewards::<T>::get(era, validator).len() < overview.page_count as usize
})
.unwrap_or(false)
}
pub(crate) fn get_paged_exposure(
era: EraIndex,
validator: &T::AccountId,
page: Page,
) -> Option<PagedExposure<T::AccountId, BalanceOf<T>>> {
let overview = <ErasStakersOverview<T>>::get(&era, validator)?;
let validator_stake = if page == 0 { overview.own } else { Zero::zero() };
let exposure_page = <ErasStakersPaged<T>>::get((era, validator, page)).unwrap_or_default();
Some(PagedExposure {
exposure_metadata: PagedExposureMetadata { own: validator_stake, ..overview },
exposure_page: exposure_page.into(),
})
}
pub(crate) fn get_full_exposure(
era: EraIndex,
validator: &T::AccountId,
) -> Exposure<T::AccountId, BalanceOf<T>> {
let Some(overview) = <ErasStakersOverview<T>>::get(&era, validator) else {
return Exposure::default();
};
let mut others = Vec::with_capacity(overview.nominator_count as usize);
for page in 0..overview.page_count {
let nominators = <ErasStakersPaged<T>>::get((era, validator, page));
others.append(&mut nominators.map(|n| n.others.clone()).defensive_unwrap_or_default());
}
Exposure { total: overview.total, own: overview.own, others }
}
pub(crate) fn exposure_page_count(era: EraIndex, validator: &T::AccountId) -> Page {
<ErasStakersOverview<T>>::get(&era, validator)
.map(|overview| {
if overview.page_count == 0 && overview.own > Zero::zero() {
1
} else {
overview.page_count
}
})
.unwrap_or(1)
}
pub(crate) fn get_next_claimable_page(era: EraIndex, validator: &T::AccountId) -> Option<Page> {
let page_count = Self::exposure_page_count(era, validator);
let all_claimable_pages: Vec<Page> = (0..page_count).collect();
let claimed_pages = ErasClaimedRewards::<T>::get(era, validator);
all_claimable_pages.into_iter().find(|p| !claimed_pages.contains(p))
}
pub(crate) fn set_rewards_as_claimed(era: EraIndex, validator: &T::AccountId, page: Page) {
let mut claimed_pages = ErasClaimedRewards::<T>::get(era, validator).into_inner();
if claimed_pages.contains(&page) {
defensive!("Trying to set an already claimed reward");
return
}
claimed_pages.push(page);
ErasClaimedRewards::<T>::insert(
era,
validator,
WeakBoundedVec::<_, _>::force_from(claimed_pages, Some("set_rewards_as_claimed")),
);
}
pub fn upsert_exposure(
era: EraIndex,
validator: &T::AccountId,
mut exposure: Exposure<T::AccountId, BalanceOf<T>>,
) {
let page_size = T::MaxExposurePageSize::get().defensive_max(1);
if let Some(stored_overview) = ErasStakersOverview::<T>::get(era, &validator) {
let last_page_idx = stored_overview.page_count.saturating_sub(1);
let mut last_page =
ErasStakersPaged::<T>::get((era, validator, last_page_idx)).unwrap_or_default();
let last_page_empty_slots =
T::MaxExposurePageSize::get().saturating_sub(last_page.others.len() as u32);
let exposures_append = exposure.split_others(last_page_empty_slots);
ErasStakersOverview::<T>::mutate(era, &validator, |stored| {
let new_metadata =
stored.defensive_unwrap_or_default().update_with::<T::MaxExposurePageSize>(
[&exposures_append, &exposure]
.iter()
.fold(Default::default(), |total, expo| {
total.saturating_add(expo.total.saturating_sub(expo.own))
}),
[&exposures_append, &exposure]
.iter()
.fold(Default::default(), |count, expo| {
count.saturating_add(expo.others.len() as u32)
}),
);
*stored = new_metadata.into();
});
last_page.page_total = last_page
.page_total
.saturating_add(exposures_append.total)
.saturating_sub(exposures_append.own);
last_page.others.extend(exposures_append.others);
ErasStakersPaged::<T>::insert((era, &validator, last_page_idx), last_page);
let (_, exposure_pages) = exposure.into_pages(page_size);
exposure_pages.into_iter().enumerate().for_each(|(idx, paged_exposure)| {
let append_at =
(last_page_idx.saturating_add(1).saturating_add(idx as u32)) as Page;
<ErasStakersPaged<T>>::insert((era, &validator, append_at), paged_exposure);
});
} else {
let expected_page_count = exposure
.others
.len()
.defensive_saturating_add((page_size as usize).defensive_saturating_sub(1))
.saturating_div(page_size as usize);
let (exposure_metadata, exposure_pages) = exposure.into_pages(page_size);
defensive_assert!(exposure_pages.len() == expected_page_count, "unexpected page count");
ErasStakersOverview::<T>::insert(era, &validator, exposure_metadata);
exposure_pages.into_iter().enumerate().for_each(|(idx, paged_exposure)| {
let append_at = idx as Page;
<ErasStakersPaged<T>>::insert((era, &validator, append_at), paged_exposure);
});
};
}
pub(crate) fn set_validators_reward(era: EraIndex, amount: BalanceOf<T>) {
ErasValidatorReward::<T>::insert(era, amount);
}
pub(crate) fn get_validators_reward(era: EraIndex) -> Option<BalanceOf<T>> {
ErasValidatorReward::<T>::get(era)
}
pub(crate) fn add_total_stake(era: EraIndex, stake: BalanceOf<T>) {
<ErasTotalStake<T>>::mutate(era, |total_stake| {
*total_stake += stake;
});
}
pub(crate) fn is_rewards_claimed(era: EraIndex, validator: &T::AccountId, page: Page) -> bool {
ErasClaimedRewards::<T>::get(era, validator).contains(&page)
}
pub(crate) fn reward_active_era(
validators_points: impl IntoIterator<Item = (T::AccountId, u32)>,
) {
if let Some(active_era) = ActiveEra::<T>::get() {
<ErasRewardPoints<T>>::mutate(active_era.index, |era_rewards| {
for (validator, points) in validators_points.into_iter() {
match era_rewards.individual.get_mut(&validator) {
Some(individual) => individual.saturating_accrue(points),
None => {
let _ =
era_rewards.individual.try_insert(validator, points).defensive();
},
}
era_rewards.total += points;
}
});
}
}
pub(crate) fn get_reward_points(era: EraIndex) -> EraRewardPoints<T> {
ErasRewardPoints::<T>::get(era)
}
}
#[cfg(any(feature = "try-runtime", test))]
impl<T: Config> Eras<T> {
pub(crate) fn era_present(era: EraIndex) -> Result<(), sp_runtime::TryRuntimeError> {
let e0 = ErasValidatorPrefs::<T>::iter_prefix_values(era).count() != 0;
let e1 = ErasStakersOverview::<T>::iter_prefix_values(era).count() != 0;
ensure!(e0 == e1, "ErasValidatorPrefs and ErasStakersOverview should be consistent");
let e2 = ErasTotalStake::<T>::contains_key(era);
let active_era = Rotator::<T>::active_era();
let e4 = if era.saturating_sub(1) > 0 &&
era.saturating_sub(1) > active_era.saturating_sub(T::HistoryDepth::get() + 1)
{
ErasValidatorReward::<T>::contains_key(era.saturating_sub(1))
} else {
e2
};
ensure!(e2 == e4, "era info presence not consistent");
if e2 {
Ok(())
} else {
Err("era presence mismatch".into())
}
}
pub(crate) fn era_absent(era: EraIndex) -> Result<(), sp_runtime::TryRuntimeError> {
let e0 = ErasValidatorPrefs::<T>::iter_prefix_values(era).count() != 0;
let e1 = ErasStakersPaged::<T>::iter_prefix_values((era,)).count() != 0;
let e2 = ErasStakersOverview::<T>::iter_prefix_values(era).count() != 0;
let e3 = ErasValidatorReward::<T>::contains_key(era);
let e4 = ErasTotalStake::<T>::contains_key(era);
let e6 = ErasClaimedRewards::<T>::iter_prefix_values(era).count() != 0;
let e7 = ErasRewardPoints::<T>::contains_key(era);
assert!(
vec![e0, e1, e2, e3, e4, e6, e7].windows(2).all(|w| w[0] == w[1]),
"era info absence not consistent for era {}: {}, {}, {}, {}, {}, {}, {}",
era,
e0,
e1,
e2,
e3,
e4,
e6,
e7
);
if !e0 {
Ok(())
} else {
Err("era absence mismatch".into())
}
}
pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
let active_era = Rotator::<T>::active_era();
let oldest_present_era = active_era.saturating_sub(T::HistoryDepth::get()).max(1);
let maybe_first_pruned_era =
active_era.saturating_sub(T::HistoryDepth::get()).checked_sub(One::one());
for e in oldest_present_era..=active_era {
Self::era_present(e)?
}
if let Some(first_pruned_era) = maybe_first_pruned_era {
Self::era_absent(first_pruned_era)?;
}
Ok(())
}
}
pub struct Rotator<T: Config>(core::marker::PhantomData<T>);
impl<T: Config> Rotator<T> {
#[cfg(feature = "runtime-benchmarks")]
pub(crate) fn legacy_insta_plan_era() -> Vec<T::AccountId> {
Self::plan_new_era();
<<T as Config>::ElectionProvider as ElectionProvider>::asap();
let msp = <T::ElectionProvider as ElectionProvider>::msp();
let lsp = 0;
for p in (lsp..=msp).rev() {
EraElectionPlanner::<T>::do_elect_paged(p);
}
crate::ElectableStashes::<T>::take().into_iter().collect()
}
#[cfg(any(feature = "try-runtime", test))]
pub(crate) fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
let planned = Self::planning_era();
let active = Self::active_era();
ensure!(
planned == active || planned == active + 1,
"planned era is always equal or one more than active"
);
let bonded = BondedEras::<T>::get();
ensure!(
bonded.into_iter().map(|(era, _sess)| era).collect::<Vec<_>>() ==
(active.saturating_sub(T::BondingDuration::get())..=active).collect::<Vec<_>>(),
"BondedEras range incorrect"
);
Ok(())
}
pub fn planning_era() -> EraIndex {
CurrentEra::<T>::get().unwrap_or(0)
}
pub fn active_era() -> EraIndex {
ActiveEra::<T>::get().map(|a| a.index).defensive_unwrap_or(0)
}
pub(crate) fn end_session(end_index: SessionIndex, activation_timestamp: Option<(u64, u32)>) {
let Some(active_era) = ActiveEra::<T>::get() else {
defensive!("Active era must always be available.");
return;
};
let current_planned_era = Self::planning_era();
let starting = end_index + 1;
let planning = starting + 1;
log!(
info,
"Session: end {:?}, start {:?} (ts: {:?}), plan {:?}",
end_index,
starting,
activation_timestamp,
planning
);
log!(info, "Era: active {:?}, planned {:?}", active_era.index, current_planned_era);
match activation_timestamp {
Some((time, id)) if id == current_planned_era => {
Self::start_era(active_era, starting, time);
},
Some((_time, id)) => {
crate::log!(
warn,
"received wrong ID with activation timestamp. Got {}, expected {}",
id,
current_planned_era
);
},
None => (),
}
let active_era = Self::active_era();
let should_plan_era = match ForceEra::<T>::get() {
Forcing::NotForcing => Self::is_plan_era_deadline(starting),
Forcing::ForceNew => {
ForceEra::<T>::put(Forcing::NotForcing);
true
},
Forcing::ForceAlways => true,
Forcing::ForceNone => false,
};
let has_pending_era = active_era < current_planned_era;
match (should_plan_era, has_pending_era) {
(false, _) => {
},
(true, false) => {
Self::plan_new_era();
},
(true, true) => {
crate::log!(
debug,
"time to plan a new era {}, but waiting for the activation of the previous.",
current_planned_era
);
},
}
Pallet::<T>::deposit_event(Event::SessionRotated {
starting_session: starting,
active_era: Self::active_era(),
planned_era: Self::planning_era(),
});
}
pub(crate) fn start_era(
ending_era: ActiveEraInfo,
starting_session: SessionIndex,
new_era_start_timestamp: u64,
) {
debug_assert!(CurrentEra::<T>::get().unwrap_or(0) == ending_era.index + 1);
let starting_era = ending_era.index + 1;
Self::end_era(&ending_era, new_era_start_timestamp);
Self::start_era_inc_active_era(new_era_start_timestamp);
Self::start_era_update_bonded_eras(starting_era, starting_session);
Self::cleanup_old_era(starting_era);
}
fn start_era_inc_active_era(start_timestamp: u64) {
ActiveEra::<T>::mutate(|active_era| {
let new_index = active_era.as_ref().map(|info| info.index + 1).unwrap_or(0);
log!(
debug,
"starting active era {:?} with RC-provided timestamp {:?}",
new_index,
start_timestamp
);
*active_era = Some(ActiveEraInfo { index: new_index, start: Some(start_timestamp) });
});
}
pub fn active_era_start_session_index() -> SessionIndex {
Self::era_start_session_index(Self::active_era()).defensive_unwrap_or(0)
}
pub fn era_start_session_index(era: EraIndex) -> Option<SessionIndex> {
BondedEras::<T>::get()
.into_iter()
.rev()
.find_map(|(e, s)| if e == era { Some(s) } else { None })
}
fn start_era_update_bonded_eras(starting_era: EraIndex, start_session: SessionIndex) {
let bonding_duration = T::BondingDuration::get();
BondedEras::<T>::mutate(|bonded| {
if bonded.is_full() {
let (era_removed, _) = bonded.remove(0);
debug_assert!(
era_removed <= (starting_era.saturating_sub(bonding_duration)),
"should not delete an era that is not older than bonding duration"
);
slashing::clear_era_metadata::<T>(era_removed);
}
let _ = bonded.try_push((starting_era, start_session)).defensive();
});
}
fn end_era(ending_era: &ActiveEraInfo, new_era_start: u64) {
let previous_era_start = ending_era.start.defensive_unwrap_or(new_era_start);
let era_duration = new_era_start.saturating_sub(previous_era_start);
Self::end_era_compute_payout(ending_era, era_duration);
}
fn end_era_compute_payout(ending_era: &ActiveEraInfo, era_duration: u64) {
let staked = ErasTotalStake::<T>::get(ending_era.index);
let issuance = asset::total_issuance::<T>();
log!(
debug,
"computing inflation for era {:?} with duration {:?}",
ending_era.index,
era_duration
);
let (validator_payout, remainder) =
T::EraPayout::era_payout(staked, issuance, era_duration);
let total_payout = validator_payout.saturating_add(remainder);
let max_staked_rewards = MaxStakedRewards::<T>::get().unwrap_or(Percent::from_percent(100));
let validator_payout = validator_payout.min(max_staked_rewards * total_payout);
let remainder = total_payout.saturating_sub(validator_payout);
Pallet::<T>::deposit_event(Event::<T>::EraPaid {
era_index: ending_era.index,
validator_payout,
remainder,
});
Eras::<T>::set_validators_reward(ending_era.index, validator_payout);
T::RewardRemainder::on_unbalanced(asset::issue::<T>(remainder));
}
fn plan_new_era() {
let _ = CurrentEra::<T>::try_mutate(|x| {
log!(debug, "Planning new era: {:?}, sending election start signal", x.unwrap_or(0));
let could_start_election = EraElectionPlanner::<T>::plan_new_election();
*x = Some(x.unwrap_or(0) + 1);
could_start_election
});
}
fn is_plan_era_deadline(start_session: SessionIndex) -> bool {
let planning_era_offset = T::PlanningEraOffset::get().min(T::SessionsPerEra::get());
let target_plan_era_session = T::SessionsPerEra::get().saturating_sub(planning_era_offset);
let era_start_session = Self::active_era_start_session_index();
let session_progress =
start_session.saturating_add(1).defensive_saturating_sub(era_start_session);
log!(
debug,
"Session progress within era: {:?}, target_plan_era_session: {:?}",
session_progress,
target_plan_era_session
);
session_progress >= target_plan_era_session
}
fn cleanup_old_era(starting_era: EraIndex) {
EraElectionPlanner::<T>::cleanup();
if let Some(old_era) = starting_era.checked_sub(T::HistoryDepth::get() + 1) {
log!(debug, "Removing era information for {:?}", old_era);
Eras::<T>::prune_era(old_era);
}
}
}
pub(crate) struct EraElectionPlanner<T: Config>(PhantomData<T>);
impl<T: Config> EraElectionPlanner<T> {
pub(crate) fn cleanup() {
VoterSnapshotStatus::<T>::kill();
NextElectionPage::<T>::kill();
ElectableStashes::<T>::kill();
Pallet::<T>::register_weight(T::DbWeight::get().writes(3));
}
pub(crate) fn election_pages() -> u32 {
<<T as Config>::ElectionProvider as ElectionProvider>::Pages::get()
}
pub(crate) fn plan_new_election() -> Result<(), <T::ElectionProvider as ElectionProvider>::Error>
{
T::ElectionProvider::start()
.inspect_err(|e| log!(warn, "Election provider failed to start: {:?}", e))
}
pub(crate) fn maybe_fetch_election_results() {
if let Ok(true) = T::ElectionProvider::status() {
crate::log!(
debug,
"Election provider is ready, our status is {:?}",
NextElectionPage::<T>::get()
);
debug_assert!(
CurrentEra::<T>::get().unwrap_or(0) ==
ActiveEra::<T>::get().map_or(0, |a| a.index) + 1,
"Next era must be already planned."
);
let current_page = NextElectionPage::<T>::get()
.unwrap_or(Self::election_pages().defensive_saturating_sub(1));
let maybe_next_page = current_page.checked_sub(1);
crate::log!(debug, "fetching page {:?}, next {:?}", current_page, maybe_next_page);
Self::do_elect_paged(current_page);
NextElectionPage::<T>::set(maybe_next_page);
if maybe_next_page.is_none() {
use pallet_staking_async_rc_client::RcClientInterface;
let id = CurrentEra::<T>::get().defensive_unwrap_or(0);
let prune_up_to = Self::get_prune_up_to();
crate::log!(
info,
"Send new validator set to RC. ID: {:?}, prune_up_to: {:?}",
id,
prune_up_to
);
T::RcClientInterface::validator_set(
ElectableStashes::<T>::take().into_iter().collect(),
id,
prune_up_to,
);
}
}
}
fn get_prune_up_to() -> Option<SessionIndex> {
let bonded_eras = BondedEras::<T>::get();
if bonded_eras.is_full() {
bonded_eras.first().map(|(_, first_session)| first_session.saturating_sub(1))
} else {
None
}
}
pub(crate) fn do_elect_paged(page: PageIndex) {
let election_result = T::ElectionProvider::elect(page);
match election_result {
Ok(supports) => {
let inner_processing_results = Self::do_elect_paged_inner(supports);
if let Err(not_included) = inner_processing_results {
defensive!(
"electable stashes exceeded limit, unexpected but election proceeds.\
{} stashes from election result discarded",
not_included
);
};
Pallet::<T>::deposit_event(Event::PagedElectionProceeded {
page,
result: inner_processing_results.map(|x| x as u32).map_err(|x| x as u32),
});
},
Err(e) => {
log!(warn, "election provider page failed due to {:?} (page: {})", e, page);
Pallet::<T>::deposit_event(Event::PagedElectionProceeded { page, result: Err(0) });
},
}
}
pub(crate) fn do_elect_paged_inner(
mut supports: BoundedSupportsOf<T::ElectionProvider>,
) -> Result<usize, usize> {
let planning_era = Rotator::<T>::planning_era();
match Self::add_electables(supports.iter().map(|(s, _)| s.clone())) {
Ok(added) => {
let exposures = Self::collect_exposures(supports);
let _ = Self::store_stakers_info(exposures, planning_era);
Ok(added)
},
Err(not_included_idx) => {
let not_included = supports.len().saturating_sub(not_included_idx);
log!(
warn,
"not all winners fit within the electable stashes, excluding {:?} accounts from solution.",
not_included,
);
supports.truncate(not_included_idx);
let exposures = Self::collect_exposures(supports);
let _ = Self::store_stakers_info(exposures, planning_era);
Err(not_included)
},
}
}
pub(crate) fn store_stakers_info(
exposures: BoundedExposuresOf<T>,
new_planned_era: EraIndex,
) -> BoundedVec<T::AccountId, MaxWinnersPerPageOf<T::ElectionProvider>> {
let mut total_stake_page: BalanceOf<T> = Zero::zero();
let mut elected_stashes_page = Vec::with_capacity(exposures.len());
let mut total_backers = 0u32;
exposures.into_iter().for_each(|(stash, exposure)| {
log!(
trace,
"stored exposure for stash {:?} and {:?} backers",
stash,
exposure.others.len()
);
elected_stashes_page.push(stash.clone());
total_stake_page = total_stake_page.saturating_add(exposure.total);
total_backers += exposure.others.len() as u32;
Eras::<T>::upsert_exposure(new_planned_era, &stash, exposure);
});
let elected_stashes: BoundedVec<_, MaxWinnersPerPageOf<T::ElectionProvider>> =
elected_stashes_page
.try_into()
.expect("both types are bounded by MaxWinnersPerPageOf; qed");
Eras::<T>::add_total_stake(new_planned_era, total_stake_page);
for stash in &elected_stashes {
let pref = Validators::<T>::get(stash);
Eras::<T>::set_validator_prefs(new_planned_era, stash, pref);
}
log!(
info,
"stored a page of stakers with {:?} validators and {:?} total backers for era {:?}",
elected_stashes.len(),
total_backers,
new_planned_era,
);
elected_stashes
}
fn collect_exposures(
supports: BoundedSupportsOf<T::ElectionProvider>,
) -> BoundedExposuresOf<T> {
let total_issuance = asset::total_issuance::<T>();
let to_currency = |e: frame_election_provider_support::ExtendedBalance| {
T::CurrencyToVote::to_currency(e, total_issuance)
};
supports
.into_iter()
.map(|(validator, support)| {
let mut others = Vec::with_capacity(support.voters.len());
let mut own: BalanceOf<T> = Zero::zero();
let mut total: BalanceOf<T> = Zero::zero();
support
.voters
.into_iter()
.map(|(nominator, weight)| (nominator, to_currency(weight)))
.for_each(|(nominator, stake)| {
if nominator == validator {
defensive_assert!(own == Zero::zero(), "own stake should be unique");
own = own.saturating_add(stake);
} else {
others.push(IndividualExposure { who: nominator, value: stake });
}
total = total.saturating_add(stake);
});
let exposure = Exposure { own, others, total };
(validator, exposure)
})
.try_collect()
.expect("we only map through support vector which cannot change the size; qed")
}
pub(crate) fn add_electables(
new_stashes: impl Iterator<Item = T::AccountId>,
) -> Result<usize, usize> {
ElectableStashes::<T>::mutate(|electable| {
let pre_size = electable.len();
for (idx, stash) in new_stashes.enumerate() {
if electable.try_insert(stash).is_err() {
return Err(idx);
}
}
Ok(electable.len() - pre_size)
})
}
}