Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to v14 #84

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 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
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@
"@polkadot-cloud/core": "^0.1.20",
"@polkadot-cloud/react": "^0.1.39",
"@polkadot-cloud/utils": "^0.0.11",
"@polkadot/api": "^10.9.1",
"@polkadot/keyring": "^12.1.1",
"@polkadot/rpc-provider": "^10.9.1",
"@polkadot/util": "^12.4.2",
"@polkadot/util-crypto": "12.4.2",
"@polkadot/api": "^14.2.1",
"@polkadot/keyring": "^13.2.2",
"@polkadot/rpc-provider": "^14.2.1",
"@polkadot/util": "^13.2.2",
"@polkadot/util-crypto": "13.2.2",
"@substrate/connect": "^0.7.31",
"@zondax/ledger-substrate": "^0.41.2",
"bignumber.js": "^9.1.2",
Expand Down
2 changes: 1 addition & 1 deletion src/contexts/Payouts/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const setLocalUnclaimedPayouts = (
network: NetworkName,
era: string,
who: string,
unclaimdPayouts: Record<string, string>,
unclaimdPayouts: Record<string, [number, string]>,
endEra: string
) => {
const current = JSON.parse(
Expand Down
92 changes: 48 additions & 44 deletions src/contexts/Payouts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,51 +146,47 @@ export const PayoutsProvider = ({
new BigNumber(b).minus(a).toNumber()
);

// Helper function to check which eras a validator was exposed in.
const validatorExposedEras = (validator: string) => {
const exposedEras: string[] = [];
for (const era of erasToCheck)
if (
Object.values(
Object.keys(getLocalEraExposure(network.name, era, activeAccount))
)?.[0] === validator
)
exposedEras.push(era);
return exposedEras;
};

// Fetch controllers in order to query ledgers.
const bondedResults =
await api.query.staking.bonded.multi<AnyApi>(uniqueValidators);
const validatorControllers: Record<string, string> = {};
for (let i = 0; i < bondedResults.length; i++) {
const ctlr = bondedResults[i].unwrapOr(null);
if (ctlr) validatorControllers[uniqueValidators[i]] = ctlr;
if (ctlr) {
validatorControllers[uniqueValidators[i]] = ctlr;
}
}

// Fetch ledgers to determine which eras have not yet been claimed per validator. Only includes
// eras that are in `erasToCheck`.
const ledgerResults = await api.query.staking.ledger.multi<AnyApi>(
Object.values(validatorControllers)
);
const unclaimedRewards: Record<string, string[]> = {};
for (const ledgerResult of ledgerResults) {
const ledger = ledgerResult.unwrapOr(null)?.toHuman();
if (ledger) {
// get claimed eras within `erasToCheck`.
const erasClaimed = ledger.claimedRewards
.map((e: string) => rmCommas(e))
.filter(
(e: string) =>
new BigNumber(e).isLessThanOrEqualTo(startEra) &&
new BigNumber(e).isGreaterThanOrEqualTo(endEra)
);

// filter eras yet to be claimed
unclaimedRewards[ledger.stash] = erasToCheck
.map((e) => e.toString())
.filter((r: string) => validatorExposedEras(ledger.stash).includes(r))
.filter((r: string) => !erasClaimed.includes(r));

// Accumulate calls to fetch unclaimed rewards for each era for all validators.
const unclaimedRewardsEntries = erasToCheck
.map((era) => uniqueValidators.map((v) => [era, v]))
.flat();

const results = await Promise.all(
unclaimedRewardsEntries.map(([era, v]) =>
api.query.staking.claimedRewards<AnyApi>(era, v)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this supports only new storage key, but it would be nice to support ledger.legacyClaimedRewards as well 😅

)
);

for (let i = 0; i < results.length; i++) {
const pages = results[i].toHuman() || [];
const era = unclaimedRewardsEntries[i][0];
const validator = unclaimedRewardsEntries[i][1];
const exposure = getLocalEraExposure(network.name, era, activeAccount);
const exposedPage =
exposure?.[validator]?.exposedPage !== undefined
? String(exposure[validator].exposedPage)
: undefined;

// Add to `unclaimedRewards` if payout page has not yet been claimed.
if (!pages.includes(exposedPage)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me this checks if page 0 is claimed. What I think if should do is check if

  • all pages where claimed

or

  • this era is in ledger.legacyClaimedRewards

I'm not sure how the unclaimedRewards is later used, but I assume in a sense of "nothing to claim in this era for this validator" not in "this validator has claimed his reward", is that correct?

if (unclaimedRewards?.[validator]) {
unclaimedRewards[validator].push(era);
} else {
unclaimedRewards[validator] = [era];
}
}
}

Expand All @@ -199,15 +195,19 @@ export const PayoutsProvider = ({
erasToCheck.forEach((era) => {
const eraValidators: string[] = [];
Object.entries(unclaimedRewards).forEach(([validator, eras]) => {
if (eras.includes(era)) eraValidators.push(validator);
if (eras.includes(era)) {
eraValidators.push(validator);
}
});
if (eraValidators.length > 0) unclaimedByEra[era] = eraValidators;
if (eraValidators.length > 0) {
unclaimedByEra[era] = eraValidators;
}
});

// Accumulate calls needed to fetch data to calculate rewards.
const calls: AnyApi[] = [];
Object.entries(unclaimedByEra).forEach(([era, validators]) => {
if (validators.length > 0)
if (validators.length > 0) {
calls.push(
Promise.all([
api.query.staking.erasValidatorReward<AnyApi>(era),
Expand All @@ -217,6 +217,7 @@ export const PayoutsProvider = ({
),
])
);
}
});

// Iterate calls and determine unclaimed payouts.
Expand Down Expand Up @@ -247,6 +248,7 @@ export const PayoutsProvider = ({
const staked = new BigNumber(localExposed?.staked || '0');
const total = new BigNumber(localExposed?.total || '0');
const isValidator = localExposed?.isValidator || false;
const exposedPage = localExposed?.exposedPage || 0;

// Calculate the validator's share of total era payout.
const totalRewardPoints = new BigNumber(
Expand All @@ -269,11 +271,13 @@ export const PayoutsProvider = ({
.dividedBy(total)
.plus(isValidator ? valCut : 0);

unclaimed[era] = {
...unclaimed[era],
[validator]: unclaimedPayout.toString(),
};
j++;
if (!unclaimedPayout.isZero()) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unclaimedPayout needs update - it doesn't take into account pages. Perhaps we don't need it, but I'd opt to we need it until we have a decision. @Marcin-Radecki WDYT?

unclaimed[era] = {
...unclaimed[era],
[validator]: [exposedPage, unclaimedPayout.toString()],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if exposedPage makes sense here. I think it should be an array of unclaimed pages. Now, if I'm not mistaken it's just the 0 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it’s not always 0. The value of exposedPage depends on the validator pages.

https://github.com/kryzasada/aleph-zero-dashboard/blob/a8d13dced1626caad8d0bb846257ad9af0296154/src/workers/stakers.ts#L79-L91

};
j++;
}
}

// This is not currently useful for preventing re-syncing. Need to know the eras that have
Expand Down
3 changes: 2 additions & 1 deletion src/contexts/Payouts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ export type PayoutsContextInterface = {

export type UnclaimedPayouts = Record<string, EraUnclaimedPayouts> | null;

export type EraUnclaimedPayouts = Record<string, string>;
export type EraUnclaimedPayouts = Record<string, [number, string]>;

export interface LocalValidatorExposure {
staked: string;
total: string;
share: string;
isValidator: boolean;
exposedPage: number;
}
33 changes: 26 additions & 7 deletions src/contexts/Staking/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,24 +40,43 @@ export const setLocalEraExposures = (
};

// Humanise and remove commas from fetched exposures.
export const formatRawExposures = (exposures: AnyApi) =>
exposures.map(([k, v]: AnyApi) => {
export const formatRawExposures = (
erasStakersOverview: AnyApi,
erasStakersPaged: AnyApi
) => {
const exposures = erasStakersOverview.map(([k, v]: AnyApi) => {
const keys = k.toHuman();
const { own, total, others } = v.toHuman();
const { own, total } = v.toHuman();

return {
keys: [rmCommas(keys[0]), keys[1]],
val: {
others: others.map(({ who, value }: AnyApi) => ({
who,
value: rmCommas(value),
})),
others: [],
own: rmCommas(own),
total: rmCommas(total),
},
};
});

erasStakersPaged.map(([k, v]: AnyApi) => {
const keys = k.toHuman();
const { others } = v.toHuman();

const foundItem = exposures.find((item: any) => item.keys[1] === keys[1]);

if (foundItem) {
foundItem.val.others.push(
...others.map(({ who, value }: AnyApi) => ({
who,
value: rmCommas(value),
}))
);
}
});

return exposures;
};

// Minify exposures data structure for local storage.
const minifyExposures = (exposures: Exposure[]) =>
exposures.map(({ keys, val: { others, own, total } }: AnyApi) => ({
Expand Down
63 changes: 60 additions & 3 deletions src/contexts/Staking/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
greaterThanZero,
isNotZero,
localStorageOrDefault,
rmCommas,
setStateWithRef,
} from '@polkadot-cloud/utils';
import BigNumber from 'bignumber.js';
Expand All @@ -16,6 +17,7 @@ import type { PayeeConfig, PayeeOptions } from 'contexts/Setup/types';
import type {
EraStakers,
Exposure,
ExposureOther,
StakingContextInterface,
StakingMetrics,
StakingTargets,
Expand Down Expand Up @@ -207,9 +209,7 @@ export const StakingProvider = ({
if (localExposures) {
exposures = localExposures;
} else {
exposures = formatRawExposures(
await api.query.staking.erasStakers.entries(era)
);
exposures = await getPagedErasStakers(era);
}

// For resource limitation concerns, only store the current era in local storage.
Expand Down Expand Up @@ -329,6 +329,63 @@ export const StakingProvider = ({
!activeAccount ||
(!hasController() && !isBonding() && !isNominating() && !isUnlocking());

// Fetch eras stakers from storage.
const getPagedErasStakers = async (era: string) => {
if (!api) {
return [];
}

const overview: AnyApi =
await api.query.staking.erasStakersOverview.entries(era);

const validators = overview.reduce(
(prev: Record<string, Exposure>, [keys, value]: AnyApi) => {
const validator = keys.toHuman()[1];
const { own, total } = value.toHuman();
return { ...prev, [validator]: { own, total } };
},
{}
);
const validatorKeys = Object.keys(validators);

const pagedResults = await Promise.all(
validatorKeys.map((v) =>
api.query.staking.erasStakersPaged.entries(era, v)
Comment on lines +339 to +353

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an edge case of an era in which migration happens. In that era the erasStakersOverview and erasStakersPaged will be empty - not populated with validators. In this case we should fallback to staking.erasStakers. I'm not sure if it's imperative we fix this, but until there is a decision I'd opt into fixing it @Marcin-Radecki

)
);

const result: Exposure[] = [];
let i = 0;
kryzasada marked this conversation as resolved.
Show resolved Hide resolved
for (const pagedResult of pagedResults) {
const validator = validatorKeys[i];
const { own, total } = validators[validator];
const others = pagedResult.reduce(
(prev: ExposureOther[], [, v]: AnyApi) => {
const o = v.toHuman()?.others || [];
if (!o.length) {
return prev;
}
return prev.concat(o);
},
[]
);

result.push({
keys: [rmCommas(era), validator],
val: {
total: rmCommas(total),
own: rmCommas(own),
others: others.map(({ who, value }) => ({
who,
value: rmCommas(value),
})),
},
});
i++;
}
return result;
};

// Helper function to get the lowest reward from an active validator.
const getLowestRewardFromStaker = (address: MaybeAccount) => {
const staker = eraStakersRef.current.stakers.find(
Expand Down
18 changes: 9 additions & 9 deletions src/contexts/Validators/ValidatorEntries/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { greaterThanZero, shuffle } from '@polkadot-cloud/utils';
import BigNumber from 'bignumber.js';
import React, { useEffect, useRef, useState } from 'react';
import { ValidatorCommunity } from '@polkadot-cloud/assets/validators';
import type { AnyApi, Fn, Sync } from 'types';
import type { AnyApi, AnyJson, Fn, Sync } from 'types';
import { useEffectIgnoreInitial } from '@polkadot-cloud/react/hooks';
import { useApi } from 'contexts/Api';
import { useBonded } from 'contexts/Bonded';
Expand Down Expand Up @@ -239,10 +239,11 @@ export const ValidatorsProvider = ({
// Subscribe to active session validators.
const subscribeSessionValidators = async () => {
if (!api || !isReady) return;
const unsub: AnyApi = await api.query.session.validators((v: AnyApi) => {
setSessionValidators(v.toHuman());
sessionUnsub.current = unsub;
});
const sessionValidatorsRaw: AnyApi =
await api.query.staking.validators.entries();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this should be really session validators or eras validators. Either way staking.validators is not the right storage 😅

This returns not only era's validators, but also as stated here "wannabe". I think we should use erasStakersOverview to get era's validators or session.validators to get session's validators. I'll try to figure out which one are we interested in 😅

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused. On production session.validators is used, but I think staking.erasStakersOverview makes more sense. @Marcin-Radecki WDYT?

setSessionValidators(
sessionValidatorsRaw.map(([keys]: AnyApi) => keys?.toHuman()?.toString())
);
};

// Subscribe to active parachain validators.
Expand Down Expand Up @@ -289,7 +290,7 @@ export const ValidatorsProvider = ({

return Object.fromEntries(
Object.entries(
Object.fromEntries(identities.map((k, i) => [addresses[i], k]))
Object.fromEntries(identities.map((k, i) => [addresses[i], k?.[0]]))
).filter(([, v]) => v !== null)
);
};
Expand Down Expand Up @@ -319,18 +320,17 @@ export const ValidatorsProvider = ({
await api.query.identity.identityOf.multi(
Object.values(supers).map(({ superOf }) => superOf[0])
)
).map((superIdentity) => superIdentity.toHuman());
).map((superIdentity: AnyJson) => superIdentity.toHuman());

const supersWithIdentity = Object.fromEntries(
Object.entries(supers).map(([k, v]: AnyApi, i) => [
k,
{
...v,
identity: superIdentities[i],
identity: superIdentities[i]?.[0],
},
])
);

return supersWithIdentity;
};

Expand Down
Loading