mirror of
https://github.com/pezkuwichain/pezkuwi-subquery.git
synced 2026-04-21 23:37:56 +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:
|
||||
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
@@ -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)`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user