feat: add APY computation to AH SubQuery, clean relay SubQuery from AH pool logic

This commit is contained in:
2026-02-18 06:00:45 +03:00
parent 5447579477
commit a7d11ce6a2
3 changed files with 109 additions and 58 deletions
+12
View File
@@ -61,6 +61,18 @@ dataSources:
filter:
module: nominationPools
method: UnbondingPoolSlashed
# Era changes on AH (staking pallet lives here) - old format
- handler: handleAHNewEra
kind: substrate/EventHandler
filter:
module: staking
method: StakingElection
# Era changes on AH - new format (Polkadot 2.0)
- handler: handleAHStakersElected
kind: substrate/EventHandler
filter:
module: staking
method: StakersElected
# Native transfers
- handler: handleTransfer
kind: substrate/EventHandler
+2 -57
View File
@@ -18,55 +18,6 @@ import {
let relayStakersInitialized = false;
/**
* Derive pool bonded (stash) account for a given pool ID.
* Matches Substrate: "modl" + PalletId("py/nopls") + AccountType::Bonded(0) + poolId(LE u32)
* padded to 32 bytes.
*/
function derivePoolStash(poolId: number): string {
const buf = new Uint8Array(32);
// "modl" prefix (4 bytes)
buf[0] = 0x6d; buf[1] = 0x6f; buf[2] = 0x64; buf[3] = 0x6c;
// PalletId: "py/nopls" (8 bytes)
const palletId = [0x70, 0x79, 0x2f, 0x6e, 0x6f, 0x70, 0x6c, 0x73];
for (let i = 0; i < 8; i++) buf[4 + i] = palletId[i];
// AccountType::Bonded = 0
buf[12] = 0;
// Pool ID as u32 LE
buf[13] = poolId & 0xff;
buf[14] = (poolId >> 8) & 0xff;
buf[15] = (poolId >> 16) & 0xff;
buf[16] = (poolId >> 24) & 0xff;
let hex = "0x";
for (let i = 0; i < 32; i++) hex += buf[i].toString(16).padStart(2, "0");
return api.registry.createType("AccountId", hex).toString();
}
/**
* Save pool stash accounts that appear as active nominators on relay chain
* as AH active stakers (networkId=AH, stakingType=relaychain).
* The wallet queries these to determine ACTIVE status for nomination pools.
*/
async function savePoolStashActiveStakers(activeNominators: Set<string>): Promise<number> {
const MAX_POOLS = 100;
let count = 0;
for (let poolId = 1; poolId <= MAX_POOLS; poolId++) {
const stash = derivePoolStash(poolId);
if (activeNominators.has(stash)) {
const stakerId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_RELAYCHAIN}-${stash}`;
const staker = ActiveStaker.create({
id: stakerId,
networkId: PEZKUWI_ASSET_HUB_GENESIS,
stakingType: STAKING_TYPE_RELAYCHAIN,
address: stash,
});
await staker.save();
count++;
}
}
return count;
}
/**
* Block handler: on the FIRST block processed, query the live chain state
* for all current era's elected nominators and validators, then save them
@@ -156,11 +107,8 @@ export async function handleRelayBlock(block: SubstrateBlock): Promise<void> {
await staker.save();
}
// Save pool stash accounts as AH active stakers
const poolCount = await savePoolStashActiveStakers(activeNominators);
logger.info(
`Initialized ${activeValidators.size} validators + ${activeNominators.size} nominators as active relay stakers, ${poolCount} pool stash accounts as AH stakers`,
`Initialized ${activeValidators.size} validators + ${activeNominators.size} nominators as active relay stakers`,
);
}
@@ -470,10 +418,7 @@ async function updateStakingApyAndActiveStakers(
await staker.save();
}
// Save pool stash accounts as AH active stakers
const poolCount = await savePoolStashActiveStakers(activeNominators);
logger.info(
`Era ${currentEra}: saved ${activeNominators.size} active stakers (relay), ${poolCount} pool stash accounts as AH stakers`,
`Era ${currentEra}: saved ${activeNominators.size} active stakers (relay)`,
);
}
+95 -1
View File
@@ -1,9 +1,15 @@
import { SubstrateEvent, SubstrateBlock } from "@subql/types";
import { ActiveStaker } from "../types";
import { ActiveStaker, StakingApy } from "../types";
import { Option } from "@pezkuwi/types";
import {
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 poolStakersInitialized = false;
@@ -81,6 +87,9 @@ export async function handleBlock(block: SubstrateBlock): Promise<void> {
}
logger.info(`Initialized ${count} pool stash accounts as active stakers`);
// Also compute and save APY on first block
await computeAndSaveAPY();
}
/**
@@ -144,3 +153,88 @@ export async function handlePoolUnbonded(
logger.info(`Pool ${poolId} stash removed: ${stashAddress}`);
}
}
// ===== APY Computation for Asset Hub =====
function calculateYearlyInflation(stakedPortion: number): number {
const idealStake = INFLATION_STAKE_TARGET;
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);
}
}
async function computeAndSaveAPY(): Promise<void> {
const totalIssuance = ((await api.query.balances.totalIssuance()) as any).toBigInt();
if (totalIssuance === BigInt(0)) return;
const activeEraOpt = (await api.query.staking.activeEra()) as Option<any>;
if (activeEraOpt.isNone) return;
const currentEra = activeEraOpt.unwrap().index.toNumber();
// Get all validator exposures for current era
const overviews = await api.query.staking.erasStakersOverview.entries(currentEra);
let totalStaked = BigInt(0);
const validators: { totalStake: bigint; commission: number }[] = [];
const validatorAddresses: string[] = [];
for (const [key, exp] of overviews) {
const [, validatorId] = key.args;
const exposure = (exp as Option<any>).unwrap();
const total = exposure.total.toBigInt();
totalStaked += total;
validatorAddresses.push(validatorId.toString());
validators.push({ totalStake: total, commission: 0 });
}
if (validators.length === 0 || totalStaked === BigInt(0)) return;
// Get commissions
const prefs = await api.query.staking.validators.multi(validatorAddresses);
for (let i = 0; i < prefs.length; i++) {
const p = prefs[i] as any;
validators[i].commission = p.commission ? Number(p.commission.toString()) / PERBILL_DIVISOR : 0;
}
// Calculate APY
const SCALE = BigInt(1_000_000_000);
const stakedPortion = Number((totalStaked * SCALE) / totalIssuance) / Number(SCALE);
const yearlyInflation = calculateYearlyInflation(stakedPortion);
const avgRewardPct = yearlyInflation / stakedPortion;
const avgStake = totalStaked / BigInt(validators.length);
let maxAPY = 0;
for (const v of validators) {
if (v.totalStake === BigInt(0)) continue;
const stakeRatio = Number((avgStake * SCALE) / v.totalStake) / Number(SCALE);
const apy = avgRewardPct * stakeRatio * (1 - v.commission);
if (apy > maxAPY) maxAPY = apy;
}
// Save APY for AH relaychain staking
const ahRelayApyId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_RELAYCHAIN}`;
await StakingApy.create({ id: ahRelayApyId, networkId: PEZKUWI_ASSET_HUB_GENESIS, stakingType: STAKING_TYPE_RELAYCHAIN, maxAPY }).save();
// Save APY for AH nomination-pool staking
const ahPoolApyId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_NOMINATION_POOL}`;
await StakingApy.create({ id: ahPoolApyId, networkId: PEZKUWI_ASSET_HUB_GENESIS, stakingType: STAKING_TYPE_NOMINATION_POOL, maxAPY }).save();
logger.info(`AH APY: ${(maxAPY * 100).toFixed(2)}% from ${validators.length} validators, era ${currentEra}`);
}
/**
* Handle staking.StakersElected on Asset Hub - recompute APY each era
*/
export async function handleAHStakersElected(event: SubstrateEvent): Promise<void> {
await computeAndSaveAPY();
}
/**
* Handle staking.StakingElection on Asset Hub (old format)
*/
export async function handleAHNewEra(event: SubstrateEvent): Promise<void> {
await computeAndSaveAPY();
}