mirror of
https://github.com/pezkuwichain/pezkuwi-subquery.git
synced 2026-04-22 04:17:59 +00:00
feat: add APY computation to AH SubQuery, clean relay SubQuery from AH pool logic
This commit is contained in:
@@ -61,6 +61,18 @@ dataSources:
|
|||||||
filter:
|
filter:
|
||||||
module: nominationPools
|
module: nominationPools
|
||||||
method: UnbondingPoolSlashed
|
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
|
# Native transfers
|
||||||
- handler: handleTransfer
|
- handler: handleTransfer
|
||||||
kind: substrate/EventHandler
|
kind: substrate/EventHandler
|
||||||
|
|||||||
+2
-57
@@ -18,55 +18,6 @@ import {
|
|||||||
|
|
||||||
let relayStakersInitialized = false;
|
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
|
* Block handler: on the FIRST block processed, query the live chain state
|
||||||
* for all current era's elected nominators and validators, then save them
|
* 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();
|
await staker.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save pool stash accounts as AH active stakers
|
|
||||||
const poolCount = await savePoolStashActiveStakers(activeNominators);
|
|
||||||
|
|
||||||
logger.info(
|
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();
|
await staker.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save pool stash accounts as AH active stakers
|
|
||||||
const poolCount = await savePoolStashActiveStakers(activeNominators);
|
|
||||||
|
|
||||||
logger.info(
|
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)`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { SubstrateEvent, SubstrateBlock } from "@subql/types";
|
import { SubstrateEvent, SubstrateBlock } from "@subql/types";
|
||||||
import { ActiveStaker } from "../types";
|
import { ActiveStaker, StakingApy } from "../types";
|
||||||
import { Option } from "@pezkuwi/types";
|
import { Option } from "@pezkuwi/types";
|
||||||
import {
|
import {
|
||||||
PEZKUWI_ASSET_HUB_GENESIS,
|
PEZKUWI_ASSET_HUB_GENESIS,
|
||||||
STAKING_TYPE_RELAYCHAIN,
|
STAKING_TYPE_RELAYCHAIN,
|
||||||
|
STAKING_TYPE_NOMINATION_POOL,
|
||||||
|
INFLATION_FALLOFF,
|
||||||
|
INFLATION_MAX,
|
||||||
|
INFLATION_MIN,
|
||||||
|
INFLATION_STAKE_TARGET,
|
||||||
|
PERBILL_DIVISOR,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
|
|
||||||
let poolStakersInitialized = false;
|
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`);
|
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}`);
|
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();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user