mirror of
https://github.com/pezkuwichain/pezkuwi-subquery.git
synced 2026-06-12 19:31:03 +00:00
Refactor: remove dead relay staking code, add AH reward handlers
- Delete NewEra.ts (all relay staking code was dead) - Refactor Rewards.ts: remove relay/parachain handlers, add handleAHRewarded/handleAHSlashed for Asset Hub staking rewards - Add staking::Rewarded and staking::Slashed handlers to pezkuwi-assethub.yaml - Remove dead cachedStakingRewardEraIndex from Cache.ts - Clean unused PEZKUWI_RELAY_GENESIS import from PoolRewards.ts
This commit is contained in:
@@ -61,6 +61,18 @@ dataSources:
|
|||||||
filter:
|
filter:
|
||||||
module: nominationPools
|
module: nominationPools
|
||||||
method: UnbondingPoolSlashed
|
method: UnbondingPoolSlashed
|
||||||
|
# Staking rewards on AH (validators + direct nominators)
|
||||||
|
- handler: handleAHRewarded
|
||||||
|
kind: substrate/EventHandler
|
||||||
|
filter:
|
||||||
|
module: staking
|
||||||
|
method: Rewarded
|
||||||
|
# Staking slashes on AH
|
||||||
|
- handler: handleAHSlashed
|
||||||
|
kind: substrate/EventHandler
|
||||||
|
filter:
|
||||||
|
module: staking
|
||||||
|
method: Slashed
|
||||||
# Era changes on AH (staking pallet lives here) - old format
|
# Era changes on AH (staking pallet lives here) - old format
|
||||||
- handler: handleAHNewEra
|
- handler: handleAHNewEra
|
||||||
kind: substrate/EventHandler
|
kind: substrate/EventHandler
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ export * from "./mappings/HistoryElements";
|
|||||||
export * from "./mappings/Rewards";
|
export * from "./mappings/Rewards";
|
||||||
export * from "./mappings/PoolRewards";
|
export * from "./mappings/PoolRewards";
|
||||||
export * from "./mappings/Transfers";
|
export * from "./mappings/Transfers";
|
||||||
export * from "./mappings/NewEra";
|
|
||||||
export * from "./mappings/PoolStakers";
|
export * from "./mappings/PoolStakers";
|
||||||
export * from "./mappings/Governance";
|
export * from "./mappings/Governance";
|
||||||
import "@pezkuwi/api-augment";
|
import "@pezkuwi/api-augment";
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ let rewardDestinationByAddress: {
|
|||||||
let controllersByStash: { [blockId: string]: { [address: string]: string } } =
|
let controllersByStash: { [blockId: string]: { [address: string]: string } } =
|
||||||
{};
|
{};
|
||||||
|
|
||||||
let parachainStakingRewardEra: { [blockId: string]: number } = {};
|
|
||||||
|
|
||||||
let poolMembers: {
|
let poolMembers: {
|
||||||
[blockId: number]: [string, any][];
|
[blockId: number]: [string, any][];
|
||||||
} = {};
|
} = {};
|
||||||
@@ -183,29 +181,6 @@ export async function cachedController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cachedStakingRewardEraIndex(
|
|
||||||
event: SubstrateEvent,
|
|
||||||
): Promise<number> {
|
|
||||||
const blockId = blockNumber(event);
|
|
||||||
let cachedEra = parachainStakingRewardEra[blockId];
|
|
||||||
|
|
||||||
if (cachedEra !== undefined) {
|
|
||||||
return cachedEra;
|
|
||||||
} else {
|
|
||||||
const era = await api.query.parachainStaking.round();
|
|
||||||
|
|
||||||
const paymentDelay =
|
|
||||||
api.consts.parachainStaking.rewardPaymentDelay.toHuman();
|
|
||||||
// HACK: used to get data from object
|
|
||||||
const eraIndex =
|
|
||||||
(era.toJSON() as { current: number }).current - Number(paymentDelay);
|
|
||||||
|
|
||||||
parachainStakingRewardEra = {};
|
|
||||||
parachainStakingRewardEra[blockId] = eraIndex;
|
|
||||||
return eraIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPoolMembers(
|
export async function getPoolMembers(
|
||||||
blockId: number,
|
blockId: number,
|
||||||
): Promise<[string, any][]> {
|
): Promise<[string, any][]> {
|
||||||
|
|||||||
@@ -1,452 +0,0 @@
|
|||||||
import { SubstrateEvent, SubstrateBlock } from "@subql/types";
|
|
||||||
import { eventId } from "./common";
|
|
||||||
import { EraValidatorInfo, StakingApy, ActiveStaker } from "../types";
|
|
||||||
import { IndividualExposure } from "../types";
|
|
||||||
import { Option } from "@pezkuwi/types";
|
|
||||||
import { Exposure } from "@pezkuwi/types/interfaces/staking";
|
|
||||||
import {
|
|
||||||
PEZKUWI_RELAY_GENESIS,
|
|
||||||
PEZKUWI_ASSET_HUB_GENESIS,
|
|
||||||
STAKING_TYPE_RELAYCHAIN,
|
|
||||||
STAKING_TYPE_NOMINATION_POOL,
|
|
||||||
INFLATION_FALLOFF,
|
|
||||||
INFLATION_MAX,
|
|
||||||
INFLATION_MIN,
|
|
||||||
INFLATION_STAKE_TARGET,
|
|
||||||
PERBILL_DIVISOR,
|
|
||||||
} from "./constants";
|
|
||||||
|
|
||||||
let relayStakersInitialized = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Block handler: on the FIRST block processed, query the live chain state
|
|
||||||
* for all current era's elected nominators and validators, then save them
|
|
||||||
* as ActiveStakers. This ensures existing stakers are captured even if
|
|
||||||
* StakersElected events were missed or had parsing issues.
|
|
||||||
*/
|
|
||||||
export async function handleRelayBlock(block: SubstrateBlock): Promise<void> {
|
|
||||||
if (relayStakersInitialized) return;
|
|
||||||
relayStakersInitialized = true;
|
|
||||||
|
|
||||||
logger.info("Initializing active relay stakers from live chain state...");
|
|
||||||
|
|
||||||
// Safety: staking pallet was removed from relay chain in spec 1_020_006
|
|
||||||
if (!api.query.staking || !api.query.staking.activeEra) {
|
|
||||||
logger.info(
|
|
||||||
"Staking pallet not available on relay chain - skipping relay staker init",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let activeEraOpt: Option<any>;
|
|
||||||
try {
|
|
||||||
activeEraOpt = (await api.query.staking.activeEra()) as Option<any>;
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn(`Failed to query staking.activeEra on relay: ${e}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (activeEraOpt.isNone) {
|
|
||||||
logger.info("No active era found on relay chain");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const currentEra = activeEraOpt.unwrap().index.toNumber();
|
|
||||||
logger.info(`Current active era: ${currentEra}`);
|
|
||||||
|
|
||||||
const activeNominators = new Set<string>();
|
|
||||||
const activeValidators = new Set<string>();
|
|
||||||
|
|
||||||
// Read all validators from overview (includes validators with only self-stake)
|
|
||||||
const overviews =
|
|
||||||
await api.query.staking.erasStakersOverview.entries(currentEra);
|
|
||||||
for (const [key, ov] of overviews) {
|
|
||||||
const [, validatorId] = key.args;
|
|
||||||
activeValidators.add(validatorId.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read all paged exposure entries for current era (contains nominators)
|
|
||||||
const pages = await api.query.staking.erasStakersPaged.entries(currentEra);
|
|
||||||
for (const [key, exp] of pages) {
|
|
||||||
const [, validatorId] = key.args;
|
|
||||||
activeValidators.add(validatorId.toString());
|
|
||||||
|
|
||||||
let exposure: any;
|
|
||||||
try {
|
|
||||||
const asOpt = exp as Option<any>;
|
|
||||||
if (asOpt.isNone) continue;
|
|
||||||
exposure = asOpt.unwrap();
|
|
||||||
} catch {
|
|
||||||
exposure = exp as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exposure.others) {
|
|
||||||
for (const other of exposure.others) {
|
|
||||||
activeNominators.add(other.who.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: if overview had no results, try legacy erasStakersClipped
|
|
||||||
if (activeValidators.size === 0) {
|
|
||||||
const clipped =
|
|
||||||
await api.query.staking.erasStakersClipped.entries(currentEra);
|
|
||||||
for (const [key, exposure] of clipped) {
|
|
||||||
const [, validatorId] = key.args;
|
|
||||||
activeValidators.add(validatorId.toString());
|
|
||||||
const exp = exposure as unknown as Exposure;
|
|
||||||
for (const other of exp.others) {
|
|
||||||
activeNominators.add(other.who.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save validators as active stakers
|
|
||||||
for (const address of activeValidators) {
|
|
||||||
const stakerId = `${PEZKUWI_RELAY_GENESIS}-${STAKING_TYPE_RELAYCHAIN}-${address}`;
|
|
||||||
const staker = ActiveStaker.create({
|
|
||||||
id: stakerId,
|
|
||||||
networkId: PEZKUWI_RELAY_GENESIS,
|
|
||||||
stakingType: STAKING_TYPE_RELAYCHAIN,
|
|
||||||
address,
|
|
||||||
});
|
|
||||||
await staker.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save nominators as active stakers
|
|
||||||
for (const address of activeNominators) {
|
|
||||||
const stakerId = `${PEZKUWI_RELAY_GENESIS}-${STAKING_TYPE_RELAYCHAIN}-${address}`;
|
|
||||||
const staker = ActiveStaker.create({
|
|
||||||
id: stakerId,
|
|
||||||
networkId: PEZKUWI_RELAY_GENESIS,
|
|
||||||
stakingType: STAKING_TYPE_RELAYCHAIN,
|
|
||||||
address,
|
|
||||||
});
|
|
||||||
await staker.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Initialized ${activeValidators.size} validators + ${activeNominators.size} nominators as active relay stakers`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleStakersElected(
|
|
||||||
event: SubstrateEvent,
|
|
||||||
): Promise<void> {
|
|
||||||
await handleNewEra(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleNewEra(event: SubstrateEvent): Promise<void> {
|
|
||||||
// Safety: staking pallet was removed from relay chain in spec 1_020_006
|
|
||||||
if (!api.query.staking || !api.query.staking.currentEra) {
|
|
||||||
logger.warn("Staking pallet not available - skipping handleNewEra");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentEra: number;
|
|
||||||
try {
|
|
||||||
currentEra = ((await api.query.staking.currentEra()) as Option<any>)
|
|
||||||
.unwrap()
|
|
||||||
.toNumber();
|
|
||||||
} catch (e) {
|
|
||||||
logger.warn(`Failed to query staking.currentEra: ${e}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let validatorExposures: Array<{
|
|
||||||
address: string;
|
|
||||||
total: bigint;
|
|
||||||
own: bigint;
|
|
||||||
others: IndividualExposure[];
|
|
||||||
}>;
|
|
||||||
|
|
||||||
if (api.query.staking.erasStakersOverview) {
|
|
||||||
validatorExposures = await processEraStakersPaged(event, currentEra);
|
|
||||||
} else {
|
|
||||||
validatorExposures = await processEraStakersClipped(event, currentEra);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute and save APY + active stakers
|
|
||||||
await updateStakingApyAndActiveStakers(currentEra, validatorExposures);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ValidatorExposureData {
|
|
||||||
address: string;
|
|
||||||
total: bigint;
|
|
||||||
own: bigint;
|
|
||||||
others: IndividualExposure[];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processEraStakersClipped(
|
|
||||||
event: SubstrateEvent,
|
|
||||||
currentEra: number,
|
|
||||||
): Promise<ValidatorExposureData[]> {
|
|
||||||
const exposures =
|
|
||||||
await api.query.staking.erasStakersClipped.entries(currentEra);
|
|
||||||
|
|
||||||
const result: ValidatorExposureData[] = [];
|
|
||||||
|
|
||||||
for (const [key, exposure] of exposures) {
|
|
||||||
const [, validatorId] = key.args;
|
|
||||||
let validatorIdString = validatorId.toString();
|
|
||||||
const exp = exposure as unknown as Exposure;
|
|
||||||
const others = exp.others.map((other) => {
|
|
||||||
return {
|
|
||||||
who: other.who.toString(),
|
|
||||||
value: other.value.toString(),
|
|
||||||
} as IndividualExposure;
|
|
||||||
});
|
|
||||||
|
|
||||||
const eraValidatorInfo = new EraValidatorInfo(
|
|
||||||
eventId(event) + validatorIdString,
|
|
||||||
validatorIdString,
|
|
||||||
currentEra,
|
|
||||||
exp.total.toBigInt(),
|
|
||||||
exp.own.toBigInt(),
|
|
||||||
others,
|
|
||||||
);
|
|
||||||
await eraValidatorInfo.save();
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
address: validatorIdString,
|
|
||||||
total: exp.total.toBigInt(),
|
|
||||||
own: exp.own.toBigInt(),
|
|
||||||
others,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processEraStakersPaged(
|
|
||||||
event: SubstrateEvent,
|
|
||||||
currentEra: number,
|
|
||||||
): Promise<ValidatorExposureData[]> {
|
|
||||||
const overview =
|
|
||||||
await api.query.staking.erasStakersOverview.entries(currentEra);
|
|
||||||
const pages = await api.query.staking.erasStakersPaged.entries(currentEra);
|
|
||||||
|
|
||||||
interface AccumulatorType {
|
|
||||||
[key: string]: { [page: number]: IndividualExposure[] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const othersCounted = pages.reduce(
|
|
||||||
(accumulator: AccumulatorType, [key, exp]) => {
|
|
||||||
const exposure = (exp as Option<any>).unwrap();
|
|
||||||
const [, validatorId, pageId] = key.args;
|
|
||||||
const pageNumber = (pageId as any).toNumber();
|
|
||||||
const validatorIdString = validatorId.toString();
|
|
||||||
|
|
||||||
const others: IndividualExposure[] = exposure.others.map(
|
|
||||||
({ who, value }: any) => {
|
|
||||||
return {
|
|
||||||
who: who.toString(),
|
|
||||||
value: value.toString(),
|
|
||||||
} as IndividualExposure;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
(accumulator[validatorIdString] = accumulator[validatorIdString] || {})[
|
|
||||||
pageNumber
|
|
||||||
] = others;
|
|
||||||
return accumulator;
|
|
||||||
},
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
const result: ValidatorExposureData[] = [];
|
|
||||||
|
|
||||||
for (const [key, exp] of overview) {
|
|
||||||
const exposure = (exp as Option<any>).unwrap();
|
|
||||||
const [, validatorId] = key.args;
|
|
||||||
let validatorIdString = validatorId.toString();
|
|
||||||
|
|
||||||
let others: IndividualExposure[] = [];
|
|
||||||
for (let i = 0; i < exposure.pageCount.toNumber(); ++i) {
|
|
||||||
others.push(...othersCounted[validatorIdString][i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const eraValidatorInfo = new EraValidatorInfo(
|
|
||||||
eventId(event) + validatorIdString,
|
|
||||||
validatorIdString,
|
|
||||||
currentEra,
|
|
||||||
exposure.total.toBigInt(),
|
|
||||||
exposure.own.toBigInt(),
|
|
||||||
others,
|
|
||||||
);
|
|
||||||
await eraValidatorInfo.save();
|
|
||||||
|
|
||||||
result.push({
|
|
||||||
address: validatorIdString,
|
|
||||||
total: exposure.total.toBigInt(),
|
|
||||||
own: exposure.own.toBigInt(),
|
|
||||||
others,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== APY Calculation (Substrate inflation curve) =====
|
|
||||||
|
|
||||||
function calculateYearlyInflation(stakedPortion: number): number {
|
|
||||||
const idealStake = INFLATION_STAKE_TARGET; // No parachains on Pezkuwi
|
|
||||||
const idealInterest = INFLATION_MAX / idealStake;
|
|
||||||
|
|
||||||
if (stakedPortion >= 0 && stakedPortion <= idealStake) {
|
|
||||||
return (
|
|
||||||
INFLATION_MIN +
|
|
||||||
stakedPortion * (idealInterest - INFLATION_MIN / idealStake)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
INFLATION_MIN +
|
|
||||||
(idealInterest * idealStake - INFLATION_MIN) *
|
|
||||||
Math.pow(2, (idealStake - stakedPortion) / INFLATION_FALLOFF)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ValidatorAPYData {
|
|
||||||
totalStake: bigint;
|
|
||||||
commission: number; // 0.0 to 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
function calculateMaxAPY(
|
|
||||||
totalIssuance: bigint,
|
|
||||||
validators: ValidatorAPYData[],
|
|
||||||
): number {
|
|
||||||
if (validators.length === 0 || totalIssuance === BigInt(0)) return 0;
|
|
||||||
|
|
||||||
const totalStaked = validators.reduce(
|
|
||||||
(sum, v) => sum + v.totalStake,
|
|
||||||
BigInt(0),
|
|
||||||
);
|
|
||||||
if (totalStaked === BigInt(0)) return 0;
|
|
||||||
|
|
||||||
// Use scaled division for precision with large BigInts
|
|
||||||
const SCALE = BigInt(1_000_000_000);
|
|
||||||
const stakedPortion =
|
|
||||||
Number((totalStaked * SCALE) / totalIssuance) / Number(SCALE);
|
|
||||||
|
|
||||||
const yearlyInflation = calculateYearlyInflation(stakedPortion);
|
|
||||||
const averageValidatorRewardPercentage = yearlyInflation / stakedPortion;
|
|
||||||
const averageValidatorStake = totalStaked / BigInt(validators.length);
|
|
||||||
|
|
||||||
let maxAPY = 0;
|
|
||||||
for (const v of validators) {
|
|
||||||
if (v.totalStake === BigInt(0)) continue;
|
|
||||||
const stakeRatio =
|
|
||||||
Number((averageValidatorStake * SCALE) / v.totalStake) / Number(SCALE);
|
|
||||||
const yearlyRewardPercentage =
|
|
||||||
averageValidatorRewardPercentage * stakeRatio;
|
|
||||||
const apy = yearlyRewardPercentage * (1 - v.commission);
|
|
||||||
if (apy > maxAPY) maxAPY = apy;
|
|
||||||
}
|
|
||||||
|
|
||||||
return maxAPY;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateStakingApyAndActiveStakers(
|
|
||||||
currentEra: number,
|
|
||||||
validatorExposures: ValidatorExposureData[],
|
|
||||||
): Promise<void> {
|
|
||||||
if (validatorExposures.length === 0) return;
|
|
||||||
|
|
||||||
// 1. Get total issuance from the relay chain
|
|
||||||
const totalIssuance = (
|
|
||||||
(await api.query.balances.totalIssuance()) as any
|
|
||||||
).toBigInt();
|
|
||||||
|
|
||||||
// 2. Get validator commissions
|
|
||||||
const validatorAddresses = validatorExposures.map((v) => v.address);
|
|
||||||
const validatorPrefs =
|
|
||||||
await api.query.staking.validators.multi(validatorAddresses);
|
|
||||||
|
|
||||||
const validatorsWithCommission: ValidatorAPYData[] = validatorExposures.map(
|
|
||||||
(v, i) => {
|
|
||||||
const prefs = validatorPrefs[i] as any;
|
|
||||||
const commissionPerbill = prefs.commission
|
|
||||||
? Number(prefs.commission.toString())
|
|
||||||
: 0;
|
|
||||||
return {
|
|
||||||
totalStake: v.total,
|
|
||||||
commission: commissionPerbill / PERBILL_DIVISOR,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// 3. Calculate maxAPY
|
|
||||||
const maxAPY = calculateMaxAPY(totalIssuance, validatorsWithCommission);
|
|
||||||
|
|
||||||
// 4. Save StakingApy for relay chain (relaychain staking)
|
|
||||||
const relayApyId = `${PEZKUWI_RELAY_GENESIS}-${STAKING_TYPE_RELAYCHAIN}`;
|
|
||||||
const relayApy = StakingApy.create({
|
|
||||||
id: relayApyId,
|
|
||||||
networkId: PEZKUWI_RELAY_GENESIS,
|
|
||||||
stakingType: STAKING_TYPE_RELAYCHAIN,
|
|
||||||
maxAPY,
|
|
||||||
});
|
|
||||||
await relayApy.save();
|
|
||||||
|
|
||||||
// 5. Save StakingApy for Asset Hub (relaychain staking option)
|
|
||||||
const ahRelayApyId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_RELAYCHAIN}`;
|
|
||||||
const ahRelayApy = StakingApy.create({
|
|
||||||
id: ahRelayApyId,
|
|
||||||
networkId: PEZKUWI_ASSET_HUB_GENESIS,
|
|
||||||
stakingType: STAKING_TYPE_RELAYCHAIN,
|
|
||||||
maxAPY,
|
|
||||||
});
|
|
||||||
await ahRelayApy.save();
|
|
||||||
|
|
||||||
// 6. Save StakingApy for Asset Hub (nomination-pool staking option)
|
|
||||||
const ahPoolApyId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_NOMINATION_POOL}`;
|
|
||||||
const ahPoolApy = StakingApy.create({
|
|
||||||
id: ahPoolApyId,
|
|
||||||
networkId: PEZKUWI_ASSET_HUB_GENESIS,
|
|
||||||
stakingType: STAKING_TYPE_NOMINATION_POOL,
|
|
||||||
maxAPY,
|
|
||||||
});
|
|
||||||
await ahPoolApy.save();
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Era ${currentEra}: maxAPY=${(maxAPY * 100).toFixed(2)}% validators=${
|
|
||||||
validatorExposures.length
|
|
||||||
} totalIssuance=${totalIssuance}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 7. Collect all unique nominator addresses from exposures (active stakers)
|
|
||||||
const activeNominators = new Set<string>();
|
|
||||||
for (const v of validatorExposures) {
|
|
||||||
for (const nominator of v.others) {
|
|
||||||
activeNominators.add(nominator.who);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Clear previous active stakers and save new ones
|
|
||||||
// For relay chain direct staking
|
|
||||||
for (const address of activeNominators) {
|
|
||||||
const relayStakerId = `${PEZKUWI_RELAY_GENESIS}-${STAKING_TYPE_RELAYCHAIN}-${address}`;
|
|
||||||
const staker = ActiveStaker.create({
|
|
||||||
id: relayStakerId,
|
|
||||||
networkId: PEZKUWI_RELAY_GENESIS,
|
|
||||||
stakingType: STAKING_TYPE_RELAYCHAIN,
|
|
||||||
address,
|
|
||||||
});
|
|
||||||
await staker.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also save validators themselves as active stakers
|
|
||||||
for (const v of validatorExposures) {
|
|
||||||
const validatorStakerId = `${PEZKUWI_RELAY_GENESIS}-${STAKING_TYPE_RELAYCHAIN}-${v.address}`;
|
|
||||||
const staker = ActiveStaker.create({
|
|
||||||
id: validatorStakerId,
|
|
||||||
networkId: PEZKUWI_RELAY_GENESIS,
|
|
||||||
stakingType: STAKING_TYPE_RELAYCHAIN,
|
|
||||||
address: v.address,
|
|
||||||
});
|
|
||||||
await staker.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
`Era ${currentEra}: saved ${activeNominators.size} active stakers (relay)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
import { getPoolMembers } from "./Cache";
|
import { getPoolMembers } from "./Cache";
|
||||||
import { Option } from "@pezkuwi/types";
|
import { Option } from "@pezkuwi/types";
|
||||||
import {
|
import {
|
||||||
PEZKUWI_RELAY_GENESIS,
|
|
||||||
PEZKUWI_ASSET_HUB_GENESIS,
|
PEZKUWI_ASSET_HUB_GENESIS,
|
||||||
STAKING_TYPE_NOMINATION_POOL,
|
STAKING_TYPE_NOMINATION_POOL,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
|
|||||||
+28
-103
@@ -22,12 +22,11 @@ import {
|
|||||||
callFromProxy,
|
callFromProxy,
|
||||||
blockNumber,
|
blockNumber,
|
||||||
} from "./common";
|
} from "./common";
|
||||||
|
import { cachedRewardDestination, cachedController } from "./Cache";
|
||||||
import {
|
import {
|
||||||
cachedRewardDestination,
|
PEZKUWI_ASSET_HUB_GENESIS,
|
||||||
cachedController,
|
STAKING_TYPE_RELAYCHAIN,
|
||||||
cachedStakingRewardEraIndex,
|
} from "./constants";
|
||||||
} from "./Cache";
|
|
||||||
import { PEZKUWI_RELAY_GENESIS, STAKING_TYPE_RELAYCHAIN } from "./constants";
|
|
||||||
|
|
||||||
function isPayoutStakers(call: any): boolean {
|
function isPayoutStakers(call: any): boolean {
|
||||||
return call.method == "payoutStakers";
|
return call.method == "payoutStakers";
|
||||||
@@ -43,13 +42,11 @@ function isPayoutValidator(call: any): boolean {
|
|||||||
|
|
||||||
function extractArgsFromPayoutStakers(call: any): [string, number] {
|
function extractArgsFromPayoutStakers(call: any): [string, number] {
|
||||||
const [validatorAddressRaw, eraRaw] = call.args;
|
const [validatorAddressRaw, eraRaw] = call.args;
|
||||||
|
|
||||||
return [validatorAddressRaw.toString(), (eraRaw as any).toNumber()];
|
return [validatorAddressRaw.toString(), (eraRaw as any).toNumber()];
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractArgsFromPayoutStakersByPage(call: any): [string, number] {
|
function extractArgsFromPayoutStakersByPage(call: any): [string, number] {
|
||||||
const [validatorAddressRaw, eraRaw, _] = call.args;
|
const [validatorAddressRaw, eraRaw, _] = call.args;
|
||||||
|
|
||||||
return [validatorAddressRaw.toString(), (eraRaw as any).toNumber()];
|
return [validatorAddressRaw.toString(), (eraRaw as any).toNumber()];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,17 +55,15 @@ function extractArgsFromPayoutValidator(
|
|||||||
sender: string,
|
sender: string,
|
||||||
): [string, number] {
|
): [string, number] {
|
||||||
const [eraRaw] = call.args;
|
const [eraRaw] = call.args;
|
||||||
|
|
||||||
return [sender, (eraRaw as any).toNumber()];
|
return [sender, (eraRaw as any).toNumber()];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleRewarded(
|
/**
|
||||||
|
* Handle staking::Rewarded on Asset Hub
|
||||||
|
*/
|
||||||
|
export async function handleAHRewarded(
|
||||||
rewardEvent: SubstrateEvent,
|
rewardEvent: SubstrateEvent,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await handleReward(rewardEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleReward(rewardEvent: SubstrateEvent): Promise<void> {
|
|
||||||
await handleRewardForTxHistory(rewardEvent);
|
await handleRewardForTxHistory(rewardEvent);
|
||||||
let accumulatedReward = await updateAccumulatedReward(rewardEvent, true);
|
let accumulatedReward = await updateAccumulatedReward(rewardEvent, true);
|
||||||
await updateAccountRewards(
|
await updateAccountRewards(
|
||||||
@@ -76,11 +71,23 @@ export async function handleReward(rewardEvent: SubstrateEvent): Promise<void> {
|
|||||||
RewardType.reward,
|
RewardType.reward,
|
||||||
accumulatedReward.amount,
|
accumulatedReward.amount,
|
||||||
);
|
);
|
||||||
await saveMultiStakingReward(
|
await saveMultiStakingReward(rewardEvent, RewardType.reward);
|
||||||
rewardEvent,
|
}
|
||||||
RewardType.reward,
|
|
||||||
STAKING_TYPE_RELAYCHAIN,
|
/**
|
||||||
|
* Handle staking::Slashed on Asset Hub
|
||||||
|
*/
|
||||||
|
export async function handleAHSlashed(
|
||||||
|
slashEvent: SubstrateEvent,
|
||||||
|
): Promise<void> {
|
||||||
|
await handleSlashForTxHistory(slashEvent);
|
||||||
|
let accumulatedReward = await updateAccumulatedReward(slashEvent, false);
|
||||||
|
await updateAccountRewards(
|
||||||
|
slashEvent,
|
||||||
|
RewardType.slash,
|
||||||
|
accumulatedReward.amount,
|
||||||
);
|
);
|
||||||
|
await saveMultiStakingReward(slashEvent, RewardType.slash);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRewardForTxHistory(
|
async function handleRewardForTxHistory(
|
||||||
@@ -89,7 +96,6 @@ async function handleRewardForTxHistory(
|
|||||||
let element = await HistoryElement.get(eventId(rewardEvent));
|
let element = await HistoryElement.get(eventId(rewardEvent));
|
||||||
|
|
||||||
if (element !== undefined) {
|
if (element !== undefined) {
|
||||||
// already processed reward previously
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,32 +221,12 @@ function determinePayoutCallsArgs(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function handleSlashed(slashEvent: SubstrateEvent): Promise<void> {
|
|
||||||
await handleSlash(slashEvent);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleSlash(slashEvent: SubstrateEvent): Promise<void> {
|
|
||||||
await handleSlashForTxHistory(slashEvent);
|
|
||||||
let accumulatedReward = await updateAccumulatedReward(slashEvent, false);
|
|
||||||
await updateAccountRewards(
|
|
||||||
slashEvent,
|
|
||||||
RewardType.slash,
|
|
||||||
accumulatedReward.amount,
|
|
||||||
);
|
|
||||||
await saveMultiStakingReward(
|
|
||||||
slashEvent,
|
|
||||||
RewardType.slash,
|
|
||||||
STAKING_TYPE_RELAYCHAIN,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getValidators(era: number): Promise<Set<string>> {
|
async function getValidators(era: number): Promise<Set<string>> {
|
||||||
const eraStakersInSlashEra = await (api.query.staking.erasStakersClipped
|
const eraStakersInSlashEra = await (api.query.staking.erasStakersClipped
|
||||||
? api.query.staking.erasStakersClipped.keys(era)
|
? api.query.staking.erasStakersClipped.keys(era)
|
||||||
: api.query.staking.erasStakersOverview.keys(era));
|
: api.query.staking.erasStakersOverview.keys(era));
|
||||||
const validatorsInSlashEra = eraStakersInSlashEra.map((key: any) => {
|
const validatorsInSlashEra = eraStakersInSlashEra.map((key: any) => {
|
||||||
let [, validatorId] = key.args;
|
let [, validatorId] = key.args;
|
||||||
|
|
||||||
return validatorId.toString();
|
return validatorId.toString();
|
||||||
});
|
});
|
||||||
return new Set(validatorsInSlashEra);
|
return new Set(validatorsInSlashEra);
|
||||||
@@ -252,7 +238,6 @@ async function handleSlashForTxHistory(
|
|||||||
let element = await HistoryElement.get(eventId(slashEvent));
|
let element = await HistoryElement.get(eventId(slashEvent));
|
||||||
|
|
||||||
if (element !== undefined) {
|
if (element !== undefined) {
|
||||||
// already processed reward previously
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const eraWrapped = await api.query.staking.currentEra();
|
const eraWrapped = await api.query.staking.currentEra();
|
||||||
@@ -396,48 +381,6 @@ async function updateAccountRewards(
|
|||||||
await accountReward.save();
|
await accountReward.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleParachainRewardForTxHistory(
|
|
||||||
rewardEvent: SubstrateEvent,
|
|
||||||
): Promise<void> {
|
|
||||||
let [account, amount] = decodeDataFromReward(rewardEvent);
|
|
||||||
handleGenericForTxHistory(
|
|
||||||
rewardEvent,
|
|
||||||
account.toString(),
|
|
||||||
async (element: HistoryElement) => {
|
|
||||||
const eraIndex = await cachedStakingRewardEraIndex(rewardEvent);
|
|
||||||
|
|
||||||
const validatorEvent = rewardEvent.block.events.find(
|
|
||||||
(event) =>
|
|
||||||
event.event.section == rewardEvent.event.section &&
|
|
||||||
event.event.method == rewardEvent.event.method,
|
|
||||||
);
|
|
||||||
const validatorId = validatorEvent?.event.data[0].toString();
|
|
||||||
element.reward = {
|
|
||||||
eventIdx: rewardEvent.idx,
|
|
||||||
amount: amount.toString(),
|
|
||||||
isReward: true,
|
|
||||||
stash: account.toString(),
|
|
||||||
validator: validatorId,
|
|
||||||
era: eraIndex,
|
|
||||||
};
|
|
||||||
|
|
||||||
return element;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function handleParachainRewarded(
|
|
||||||
rewardEvent: SubstrateEvent,
|
|
||||||
): Promise<void> {
|
|
||||||
await handleParachainRewardForTxHistory(rewardEvent);
|
|
||||||
let accumulatedReward = await updateAccumulatedReward(rewardEvent, true);
|
|
||||||
await updateAccountRewards(
|
|
||||||
rewardEvent,
|
|
||||||
RewardType.reward,
|
|
||||||
accumulatedReward.amount,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============= GENERICS ================
|
// ============= GENERICS ================
|
||||||
|
|
||||||
interface AccumulatedInterface {
|
interface AccumulatedInterface {
|
||||||
@@ -500,23 +443,6 @@ export async function handleGenericForTxHistory(
|
|||||||
(await fieldCallback(element)).save();
|
(await fieldCallback(element)).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AccountRewardsInterface {
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
address: string;
|
|
||||||
|
|
||||||
blockNumber: number;
|
|
||||||
|
|
||||||
timestamp: bigint;
|
|
||||||
|
|
||||||
amount: bigint;
|
|
||||||
|
|
||||||
accumulatedAmount: bigint;
|
|
||||||
|
|
||||||
type: RewardType;
|
|
||||||
save(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function eventRecordToSubstrateEvent(eventRecord: any): SubstrateEvent {
|
export function eventRecordToSubstrateEvent(eventRecord: any): SubstrateEvent {
|
||||||
return eventRecord as unknown as SubstrateEvent;
|
return eventRecord as unknown as SubstrateEvent;
|
||||||
}
|
}
|
||||||
@@ -538,19 +464,18 @@ function decodeDataFromReward(event: SubstrateEvent): [any, any] {
|
|||||||
* Save a reward/slash to the multi-staking Reward entity
|
* Save a reward/slash to the multi-staking Reward entity
|
||||||
* (used by PezWallet dashboard for rewards aggregation)
|
* (used by PezWallet dashboard for rewards aggregation)
|
||||||
*/
|
*/
|
||||||
export async function saveMultiStakingReward(
|
async function saveMultiStakingReward(
|
||||||
event: SubstrateEvent,
|
event: SubstrateEvent,
|
||||||
rewardType: RewardType,
|
rewardType: RewardType,
|
||||||
stakingType: string,
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const [accountId, amount] = decodeDataFromReward(event);
|
const [accountId, amount] = decodeDataFromReward(event);
|
||||||
const accountAddress = accountId.toString();
|
const accountAddress = accountId.toString();
|
||||||
const id = `${eventId(event)}-${accountAddress}-${stakingType}`;
|
const id = `${eventId(event)}-${accountAddress}-${STAKING_TYPE_RELAYCHAIN}`;
|
||||||
|
|
||||||
const reward = Reward.create({
|
const reward = Reward.create({
|
||||||
id,
|
id,
|
||||||
networkId: PEZKUWI_RELAY_GENESIS,
|
networkId: PEZKUWI_ASSET_HUB_GENESIS,
|
||||||
stakingType,
|
stakingType: STAKING_TYPE_RELAYCHAIN,
|
||||||
address: accountAddress,
|
address: accountAddress,
|
||||||
type: rewardType,
|
type: rewardType,
|
||||||
amount: (amount as any).toBigInt(),
|
amount: (amount as any).toBigInt(),
|
||||||
|
|||||||
Reference in New Issue
Block a user