mirror of
https://github.com/pezkuwichain/pezkuwi-subquery.git
synced 2026-04-22 01:57:58 +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:
|
||||
module: nominationPools
|
||||
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
|
||||
- handler: handleAHNewEra
|
||||
kind: substrate/EventHandler
|
||||
|
||||
@@ -3,7 +3,6 @@ export * from "./mappings/HistoryElements";
|
||||
export * from "./mappings/Rewards";
|
||||
export * from "./mappings/PoolRewards";
|
||||
export * from "./mappings/Transfers";
|
||||
export * from "./mappings/NewEra";
|
||||
export * from "./mappings/PoolStakers";
|
||||
export * from "./mappings/Governance";
|
||||
import "@pezkuwi/api-augment";
|
||||
|
||||
@@ -10,8 +10,6 @@ let rewardDestinationByAddress: {
|
||||
let controllersByStash: { [blockId: string]: { [address: string]: string } } =
|
||||
{};
|
||||
|
||||
let parachainStakingRewardEra: { [blockId: string]: number } = {};
|
||||
|
||||
let poolMembers: {
|
||||
[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(
|
||||
blockId: number,
|
||||
): 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 { Option } from "@pezkuwi/types";
|
||||
import {
|
||||
PEZKUWI_RELAY_GENESIS,
|
||||
PEZKUWI_ASSET_HUB_GENESIS,
|
||||
STAKING_TYPE_NOMINATION_POOL,
|
||||
} from "./constants";
|
||||
|
||||
+28
-103
@@ -22,12 +22,11 @@ import {
|
||||
callFromProxy,
|
||||
blockNumber,
|
||||
} from "./common";
|
||||
import { cachedRewardDestination, cachedController } from "./Cache";
|
||||
import {
|
||||
cachedRewardDestination,
|
||||
cachedController,
|
||||
cachedStakingRewardEraIndex,
|
||||
} from "./Cache";
|
||||
import { PEZKUWI_RELAY_GENESIS, STAKING_TYPE_RELAYCHAIN } from "./constants";
|
||||
PEZKUWI_ASSET_HUB_GENESIS,
|
||||
STAKING_TYPE_RELAYCHAIN,
|
||||
} from "./constants";
|
||||
|
||||
function isPayoutStakers(call: any): boolean {
|
||||
return call.method == "payoutStakers";
|
||||
@@ -43,13 +42,11 @@ function isPayoutValidator(call: any): boolean {
|
||||
|
||||
function extractArgsFromPayoutStakers(call: any): [string, number] {
|
||||
const [validatorAddressRaw, eraRaw] = call.args;
|
||||
|
||||
return [validatorAddressRaw.toString(), (eraRaw as any).toNumber()];
|
||||
}
|
||||
|
||||
function extractArgsFromPayoutStakersByPage(call: any): [string, number] {
|
||||
const [validatorAddressRaw, eraRaw, _] = call.args;
|
||||
|
||||
return [validatorAddressRaw.toString(), (eraRaw as any).toNumber()];
|
||||
}
|
||||
|
||||
@@ -58,17 +55,15 @@ function extractArgsFromPayoutValidator(
|
||||
sender: string,
|
||||
): [string, number] {
|
||||
const [eraRaw] = call.args;
|
||||
|
||||
return [sender, (eraRaw as any).toNumber()];
|
||||
}
|
||||
|
||||
export async function handleRewarded(
|
||||
/**
|
||||
* Handle staking::Rewarded on Asset Hub
|
||||
*/
|
||||
export async function handleAHRewarded(
|
||||
rewardEvent: SubstrateEvent,
|
||||
): Promise<void> {
|
||||
await handleReward(rewardEvent);
|
||||
}
|
||||
|
||||
export async function handleReward(rewardEvent: SubstrateEvent): Promise<void> {
|
||||
await handleRewardForTxHistory(rewardEvent);
|
||||
let accumulatedReward = await updateAccumulatedReward(rewardEvent, true);
|
||||
await updateAccountRewards(
|
||||
@@ -76,11 +71,23 @@ export async function handleReward(rewardEvent: SubstrateEvent): Promise<void> {
|
||||
RewardType.reward,
|
||||
accumulatedReward.amount,
|
||||
);
|
||||
await saveMultiStakingReward(
|
||||
rewardEvent,
|
||||
RewardType.reward,
|
||||
STAKING_TYPE_RELAYCHAIN,
|
||||
await saveMultiStakingReward(rewardEvent, RewardType.reward);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
@@ -89,7 +96,6 @@ async function handleRewardForTxHistory(
|
||||
let element = await HistoryElement.get(eventId(rewardEvent));
|
||||
|
||||
if (element !== undefined) {
|
||||
// already processed reward previously
|
||||
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>> {
|
||||
const eraStakersInSlashEra = await (api.query.staking.erasStakersClipped
|
||||
? api.query.staking.erasStakersClipped.keys(era)
|
||||
: api.query.staking.erasStakersOverview.keys(era));
|
||||
const validatorsInSlashEra = eraStakersInSlashEra.map((key: any) => {
|
||||
let [, validatorId] = key.args;
|
||||
|
||||
return validatorId.toString();
|
||||
});
|
||||
return new Set(validatorsInSlashEra);
|
||||
@@ -252,7 +238,6 @@ async function handleSlashForTxHistory(
|
||||
let element = await HistoryElement.get(eventId(slashEvent));
|
||||
|
||||
if (element !== undefined) {
|
||||
// already processed reward previously
|
||||
return;
|
||||
}
|
||||
const eraWrapped = await api.query.staking.currentEra();
|
||||
@@ -396,48 +381,6 @@ async function updateAccountRewards(
|
||||
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 ================
|
||||
|
||||
interface AccumulatedInterface {
|
||||
@@ -500,23 +443,6 @@ export async function handleGenericForTxHistory(
|
||||
(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 {
|
||||
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
|
||||
* (used by PezWallet dashboard for rewards aggregation)
|
||||
*/
|
||||
export async function saveMultiStakingReward(
|
||||
async function saveMultiStakingReward(
|
||||
event: SubstrateEvent,
|
||||
rewardType: RewardType,
|
||||
stakingType: string,
|
||||
): Promise<void> {
|
||||
const [accountId, amount] = decodeDataFromReward(event);
|
||||
const accountAddress = accountId.toString();
|
||||
const id = `${eventId(event)}-${accountAddress}-${stakingType}`;
|
||||
const id = `${eventId(event)}-${accountAddress}-${STAKING_TYPE_RELAYCHAIN}`;
|
||||
|
||||
const reward = Reward.create({
|
||||
id,
|
||||
networkId: PEZKUWI_RELAY_GENESIS,
|
||||
stakingType,
|
||||
networkId: PEZKUWI_ASSET_HUB_GENESIS,
|
||||
stakingType: STAKING_TYPE_RELAYCHAIN,
|
||||
address: accountAddress,
|
||||
type: rewardType,
|
||||
amount: (amount as any).toBigInt(),
|
||||
|
||||
Reference in New Issue
Block a user