mirror of
https://github.com/pezkuwichain/pezkuwi-subquery.git
synced 2026-06-14 08:51:04 +00:00
Add multi-staking entities, APY calculation, Asset Hub indexer support
- Add StakingApy, ActiveStaker, Reward entities to schema.graphql - Add APY calculation engine in NewEra.ts (inflation curve + validator commission) - Add saveMultiStakingReward to Rewards.ts and PoolRewards.ts - Add handleAssetTransfer for assets.Transferred events - Add constants.ts with genesis hashes and inflation params - Add docker-compose.prod.yml for production deployment (relay + assethub nodes) - Add deploy-vps.yml GitHub Actions workflow for auto-deploy on push
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
name: Deploy to VPS
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.VPS_HOST }}
|
||||||
|
username: root
|
||||||
|
key: ${{ secrets.VPS_SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
cd /opt/subquery
|
||||||
|
git pull origin main
|
||||||
|
docker compose -f docker-compose.prod.yml pull
|
||||||
|
docker compose -f docker-compose.prod.yml up -d
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
version: "3.8"
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
container_name: postgres-pezkuwi
|
||||||
|
image: postgres:16-alpine
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
volumes:
|
||||||
|
- pgdata:/var/lib/postgresql/data
|
||||||
|
- ./docker/init:/docker-entrypoint-initdb.d
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: pezkuwi_subquery_2024
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
subquery-node-relay:
|
||||||
|
container_name: node-pezkuwi-relay
|
||||||
|
image: onfinality/subql-node:latest
|
||||||
|
depends_on:
|
||||||
|
postgres: { condition: service_healthy }
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASS: pezkuwi_subquery_2024
|
||||||
|
DB_DATABASE: postgres
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
volumes:
|
||||||
|
- ./:/app/project
|
||||||
|
command:
|
||||||
|
- -f=/app/project/pezkuwi.yaml
|
||||||
|
- --disable-historical=true
|
||||||
|
- --batch-size=30
|
||||||
|
|
||||||
|
subquery-node-assethub:
|
||||||
|
container_name: node-pezkuwi-assethub
|
||||||
|
image: onfinality/subql-node:latest
|
||||||
|
depends_on:
|
||||||
|
postgres: { condition: service_healthy }
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASS: pezkuwi_subquery_2024
|
||||||
|
DB_DATABASE: postgres
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
volumes:
|
||||||
|
- ./:/app/project
|
||||||
|
command:
|
||||||
|
- -f=/app/project/pezkuwi-assethub.yaml
|
||||||
|
- --disable-historical=true
|
||||||
|
- --batch-size=30
|
||||||
|
|
||||||
|
graphql-engine:
|
||||||
|
container_name: query-pezkuwi
|
||||||
|
image: onfinality/subql-query:v1.5.0
|
||||||
|
ports:
|
||||||
|
- 3000:3000
|
||||||
|
depends_on:
|
||||||
|
- subquery-node-relay
|
||||||
|
- subquery-node-assethub
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASS: pezkuwi_subquery_2024
|
||||||
|
DB_DATABASE: postgres
|
||||||
|
DB_HOST: postgres
|
||||||
|
DB_PORT: 5432
|
||||||
|
command:
|
||||||
|
- --name=subquery-pezkuwi-staking
|
||||||
|
- --playground
|
||||||
|
- --indexer=http://subquery-node-relay:3000
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pgdata:
|
||||||
+29
-2
@@ -17,7 +17,7 @@ type AssetTransfer @jsonField {
|
|||||||
success: Boolean!
|
success: Boolean!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Reward @jsonField {
|
type RewardInfo @jsonField {
|
||||||
eventIdx: Int!
|
eventIdx: Int!
|
||||||
amount: String!
|
amount: String!
|
||||||
isReward: Boolean!
|
isReward: Boolean!
|
||||||
@@ -97,7 +97,7 @@ type HistoryElement @entity {
|
|||||||
extrinsicHash: String
|
extrinsicHash: String
|
||||||
timestamp: BigInt! @index
|
timestamp: BigInt! @index
|
||||||
address: String! @index
|
address: String! @index
|
||||||
reward: Reward
|
reward: RewardInfo
|
||||||
poolReward: PoolReward
|
poolReward: PoolReward
|
||||||
extrinsic: Extrinsic
|
extrinsic: Extrinsic
|
||||||
transfer: Transfer
|
transfer: Transfer
|
||||||
@@ -123,3 +123,30 @@ type ErrorEvent @entity {
|
|||||||
id: ID!
|
id: ID!
|
||||||
description: String!
|
description: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ===== Multi-staking API entities (used by PezWallet dashboard) =====
|
||||||
|
|
||||||
|
type StakingApy @entity {
|
||||||
|
id: ID!
|
||||||
|
networkId: String! @index
|
||||||
|
stakingType: String! @index
|
||||||
|
maxAPY: Float!
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveStaker @entity {
|
||||||
|
id: ID!
|
||||||
|
networkId: String! @index
|
||||||
|
stakingType: String! @index
|
||||||
|
address: String! @index
|
||||||
|
}
|
||||||
|
|
||||||
|
type Reward @entity {
|
||||||
|
id: ID!
|
||||||
|
networkId: String! @index
|
||||||
|
stakingType: String! @index
|
||||||
|
address: String! @index
|
||||||
|
type: RewardType! @index
|
||||||
|
amount: BigInt!
|
||||||
|
timestamp: BigInt!
|
||||||
|
blockNumber: Int! @index
|
||||||
|
}
|
||||||
|
|||||||
+227
-11
@@ -1,9 +1,20 @@
|
|||||||
import { SubstrateEvent } from "@subql/types";
|
import { SubstrateEvent } from "@subql/types";
|
||||||
import { eventId } from "./common";
|
import { eventId } from "./common";
|
||||||
import { EraValidatorInfo } from "../types/models/EraValidatorInfo";
|
import { EraValidatorInfo, StakingApy, ActiveStaker } from "../types";
|
||||||
import { IndividualExposure } from "../types";
|
import { IndividualExposure } from "../types";
|
||||||
import { Option } from "@pezkuwi/types";
|
import { Option } from "@pezkuwi/types";
|
||||||
import { Exposure } from "@pezkuwi/types/interfaces/staking";
|
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";
|
||||||
|
|
||||||
export async function handleStakersElected(
|
export async function handleStakersElected(
|
||||||
event: SubstrateEvent,
|
event: SubstrateEvent,
|
||||||
@@ -16,45 +27,75 @@ export async function handleNewEra(event: SubstrateEvent): Promise<void> {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.toNumber();
|
.toNumber();
|
||||||
|
|
||||||
|
let validatorExposures: Array<{
|
||||||
|
address: string;
|
||||||
|
total: bigint;
|
||||||
|
own: bigint;
|
||||||
|
others: IndividualExposure[];
|
||||||
|
}>;
|
||||||
|
|
||||||
if (api.query.staking.erasStakersOverview) {
|
if (api.query.staking.erasStakersOverview) {
|
||||||
await processEraStakersPaged(event, currentEra);
|
validatorExposures = await processEraStakersPaged(event, currentEra);
|
||||||
} else {
|
} else {
|
||||||
await processEraStakersClipped(event, currentEra);
|
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(
|
async function processEraStakersClipped(
|
||||||
event: SubstrateEvent,
|
event: SubstrateEvent,
|
||||||
currentEra: number,
|
currentEra: number,
|
||||||
): Promise<void> {
|
): Promise<ValidatorExposureData[]> {
|
||||||
const exposures =
|
const exposures =
|
||||||
await api.query.staking.erasStakersClipped.entries(currentEra);
|
await api.query.staking.erasStakersClipped.entries(currentEra);
|
||||||
|
|
||||||
|
const result: ValidatorExposureData[] = [];
|
||||||
|
|
||||||
for (const [key, exposure] of exposures) {
|
for (const [key, exposure] of exposures) {
|
||||||
const [, validatorId] = key.args;
|
const [, validatorId] = key.args;
|
||||||
let validatorIdString = validatorId.toString();
|
let validatorIdString = validatorId.toString();
|
||||||
const exp = exposure as unknown as Exposure;
|
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(
|
const eraValidatorInfo = new EraValidatorInfo(
|
||||||
eventId(event) + validatorIdString,
|
eventId(event) + validatorIdString,
|
||||||
validatorIdString,
|
validatorIdString,
|
||||||
currentEra,
|
currentEra,
|
||||||
exp.total.toBigInt(),
|
exp.total.toBigInt(),
|
||||||
exp.own.toBigInt(),
|
exp.own.toBigInt(),
|
||||||
exp.others.map((other) => {
|
others,
|
||||||
return {
|
|
||||||
who: other.who.toString(),
|
|
||||||
value: other.value.toString(),
|
|
||||||
} as IndividualExposure;
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
await eraValidatorInfo.save();
|
await eraValidatorInfo.save();
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
address: validatorIdString,
|
||||||
|
total: exp.total.toBigInt(),
|
||||||
|
own: exp.own.toBigInt(),
|
||||||
|
others,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processEraStakersPaged(
|
async function processEraStakersPaged(
|
||||||
event: SubstrateEvent,
|
event: SubstrateEvent,
|
||||||
currentEra: number,
|
currentEra: number,
|
||||||
): Promise<void> {
|
): Promise<ValidatorExposureData[]> {
|
||||||
const overview =
|
const overview =
|
||||||
await api.query.staking.erasStakersOverview.entries(currentEra);
|
await api.query.staking.erasStakersOverview.entries(currentEra);
|
||||||
const pages = await api.query.staking.erasStakersPaged.entries(currentEra);
|
const pages = await api.query.staking.erasStakersPaged.entries(currentEra);
|
||||||
@@ -87,6 +128,8 @@ async function processEraStakersPaged(
|
|||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const result: ValidatorExposureData[] = [];
|
||||||
|
|
||||||
for (const [key, exp] of overview) {
|
for (const [key, exp] of overview) {
|
||||||
const exposure = (exp as Option<any>).unwrap();
|
const exposure = (exp as Option<any>).unwrap();
|
||||||
const [, validatorId] = key.args;
|
const [, validatorId] = key.args;
|
||||||
@@ -106,5 +149,178 @@ async function processEraStakersPaged(
|
|||||||
others,
|
others,
|
||||||
);
|
);
|
||||||
await eraValidatorInfo.save();
|
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)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import {
|
|||||||
AccumulatedReward,
|
AccumulatedReward,
|
||||||
AccumulatedPoolReward,
|
AccumulatedPoolReward,
|
||||||
HistoryElement,
|
HistoryElement,
|
||||||
|
Reward,
|
||||||
RewardType,
|
RewardType,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { SubstrateEvent } from "@subql/types";
|
import { SubstrateEvent } from "@subql/types";
|
||||||
import {
|
import {
|
||||||
|
eventId,
|
||||||
eventIdFromBlockAndIdxAndAddress,
|
eventIdFromBlockAndIdxAndAddress,
|
||||||
timestamp,
|
timestamp,
|
||||||
eventIdWithAddress,
|
eventIdWithAddress,
|
||||||
@@ -18,6 +20,11 @@ import {
|
|||||||
} from "./Rewards";
|
} from "./Rewards";
|
||||||
import { getPoolMembers } from "./Cache";
|
import { getPoolMembers } from "./Cache";
|
||||||
import { Option } from "@pezkuwi/types";
|
import { Option } from "@pezkuwi/types";
|
||||||
|
import {
|
||||||
|
PEZKUWI_RELAY_GENESIS,
|
||||||
|
PEZKUWI_ASSET_HUB_GENESIS,
|
||||||
|
STAKING_TYPE_NOMINATION_POOL,
|
||||||
|
} from "./constants";
|
||||||
|
|
||||||
export async function handlePoolReward(
|
export async function handlePoolReward(
|
||||||
rewardEvent: SubstrateEvent,
|
rewardEvent: SubstrateEvent,
|
||||||
@@ -37,6 +44,13 @@ export async function handlePoolReward(
|
|||||||
RewardType.reward,
|
RewardType.reward,
|
||||||
accumulatedReward.amount,
|
accumulatedReward.amount,
|
||||||
);
|
);
|
||||||
|
// Save to multi-staking Reward entity for both relay and Asset Hub networkIds
|
||||||
|
await savePoolMultiStakingReward(
|
||||||
|
rewardEvent,
|
||||||
|
accountId.toString(),
|
||||||
|
(amount as any).toBigInt(),
|
||||||
|
RewardType.reward,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePoolRewardForTxHistory(
|
async function handlePoolRewardForTxHistory(
|
||||||
@@ -200,6 +214,13 @@ async function handleRelaychainPooledStakingSlash(
|
|||||||
RewardType.slash,
|
RewardType.slash,
|
||||||
accumulatedReward.amount,
|
accumulatedReward.amount,
|
||||||
);
|
);
|
||||||
|
// Save to multi-staking Reward entity
|
||||||
|
await savePoolMultiStakingReward(
|
||||||
|
event,
|
||||||
|
accountId,
|
||||||
|
personalSlash,
|
||||||
|
RewardType.slash,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,3 +260,32 @@ async function handlePoolSlashForTxHistory(
|
|||||||
|
|
||||||
await element.save();
|
await element.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save pool reward/slash to the multi-staking Reward entity
|
||||||
|
* Saves for both relay chain and Asset Hub networkIds since nom pools
|
||||||
|
* are accessible from both contexts
|
||||||
|
*/
|
||||||
|
async function savePoolMultiStakingReward(
|
||||||
|
event: SubstrateEvent,
|
||||||
|
accountAddress: string,
|
||||||
|
amount: bigint,
|
||||||
|
rewardType: RewardType,
|
||||||
|
): Promise<void> {
|
||||||
|
const ts = timestamp(event.block);
|
||||||
|
const bn = blockNumber(event);
|
||||||
|
const baseId = `${eventId(event)}-${accountAddress}-pool`;
|
||||||
|
|
||||||
|
// Save for Asset Hub networkId (nomination pools are accessed via Asset Hub)
|
||||||
|
const ahReward = Reward.create({
|
||||||
|
id: `${baseId}-ah`,
|
||||||
|
networkId: PEZKUWI_ASSET_HUB_GENESIS,
|
||||||
|
stakingType: STAKING_TYPE_NOMINATION_POOL,
|
||||||
|
address: accountAddress,
|
||||||
|
type: rewardType,
|
||||||
|
amount,
|
||||||
|
timestamp: ts,
|
||||||
|
blockNumber: bn,
|
||||||
|
});
|
||||||
|
await ahReward.save();
|
||||||
|
}
|
||||||
|
|||||||
+34
-1
@@ -3,6 +3,7 @@ import {
|
|||||||
AccumulatedReward,
|
AccumulatedReward,
|
||||||
HistoryElement,
|
HistoryElement,
|
||||||
Reward,
|
Reward,
|
||||||
|
RewardInfo,
|
||||||
RewardType,
|
RewardType,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +27,10 @@ import {
|
|||||||
cachedController,
|
cachedController,
|
||||||
cachedStakingRewardEraIndex,
|
cachedStakingRewardEraIndex,
|
||||||
} from "./Cache";
|
} 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";
|
||||||
@@ -74,6 +79,7 @@ export async function handleReward(rewardEvent: SubstrateEvent): Promise<void> {
|
|||||||
RewardType.reward,
|
RewardType.reward,
|
||||||
accumulatedReward.amount,
|
accumulatedReward.amount,
|
||||||
);
|
);
|
||||||
|
await saveMultiStakingReward(rewardEvent, RewardType.reward, STAKING_TYPE_RELAYCHAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRewardForTxHistory(
|
async function handleRewardForTxHistory(
|
||||||
@@ -220,6 +226,7 @@ export async function handleSlash(slashEvent: SubstrateEvent): Promise<void> {
|
|||||||
RewardType.slash,
|
RewardType.slash,
|
||||||
accumulatedReward.amount,
|
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>> {
|
||||||
@@ -297,7 +304,7 @@ async function buildRewardEvents<A>(
|
|||||||
eventIdx: number,
|
eventIdx: number,
|
||||||
stash: string,
|
stash: string,
|
||||||
amount: string,
|
amount: string,
|
||||||
) => Reward,
|
) => RewardInfo,
|
||||||
) {
|
) {
|
||||||
let blockNum = block.block.header.number.toString();
|
let blockNum = block.block.header.number.toString();
|
||||||
let blockTimestamp = timestamp(block);
|
let blockTimestamp = timestamp(block);
|
||||||
@@ -521,3 +528,29 @@ function decodeDataFromReward(event: SubstrateEvent): [any, any] {
|
|||||||
}
|
}
|
||||||
return [account, amount];
|
return [account, amount];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a reward/slash to the multi-staking Reward entity
|
||||||
|
* (used by PezWallet dashboard for rewards aggregation)
|
||||||
|
*/
|
||||||
|
export 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 reward = Reward.create({
|
||||||
|
id,
|
||||||
|
networkId: PEZKUWI_RELAY_GENESIS,
|
||||||
|
stakingType,
|
||||||
|
address: accountAddress,
|
||||||
|
type: rewardType,
|
||||||
|
amount: (amount as any).toBigInt(),
|
||||||
|
timestamp: timestamp(event.block),
|
||||||
|
blockNumber: blockNumber(event),
|
||||||
|
});
|
||||||
|
await reward.save();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HistoryElement, Transfer } from "../types";
|
import { HistoryElement, Transfer, AssetTransfer } from "../types";
|
||||||
import { SubstrateEvent } from "@subql/types";
|
import { SubstrateEvent } from "@subql/types";
|
||||||
import {
|
import {
|
||||||
blockNumber,
|
blockNumber,
|
||||||
@@ -61,3 +61,42 @@ async function createTransfer(
|
|||||||
element.transfer = transfer;
|
element.transfer = transfer;
|
||||||
await element.save();
|
await element.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function handleAssetTransfer(event: SubstrateEvent): Promise<void> {
|
||||||
|
const [assetId, from, to, amount] = getEventData(event);
|
||||||
|
|
||||||
|
const assetTransferData: AssetTransfer = {
|
||||||
|
assetId: assetId.toString(),
|
||||||
|
amount: amount.toString(),
|
||||||
|
from: from.toString(),
|
||||||
|
to: to.toString(),
|
||||||
|
fee: calculateFeeAsString(event.extrinsic, from.toString()),
|
||||||
|
eventIdx: event.idx,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await createAssetTransferHistory(event, from.toString(), "-from", assetTransferData);
|
||||||
|
await createAssetTransferHistory(event, to.toString(), "-to", assetTransferData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createAssetTransferHistory(
|
||||||
|
event: SubstrateEvent,
|
||||||
|
address: string,
|
||||||
|
suffix: string,
|
||||||
|
data: AssetTransfer,
|
||||||
|
): Promise<void> {
|
||||||
|
const element = new HistoryElement(
|
||||||
|
`${eventId(event)}${suffix}`,
|
||||||
|
blockNumber(event),
|
||||||
|
timestamp(event.block),
|
||||||
|
address,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (event.extrinsic !== undefined) {
|
||||||
|
element.extrinsicHash = event.extrinsic.extrinsic.hash.toString();
|
||||||
|
element.extrinsicIdx = event.extrinsic.idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
element.assetTransfer = data;
|
||||||
|
await element.save();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// Pezkuwi chain genesis hashes (with 0x prefix, as the app expects)
|
||||||
|
export const PEZKUWI_RELAY_GENESIS =
|
||||||
|
"0xbb4a61ab0c4b8c12f5eab71d0c86c482e03a275ecdafee678dea712474d33d75";
|
||||||
|
export const PEZKUWI_ASSET_HUB_GENESIS =
|
||||||
|
"0x00d0e1d0581c3cd5c5768652d52f4520184018b44f56a2ae1e0dc9d65c00c948";
|
||||||
|
|
||||||
|
// Staking type identifiers (must match the app's mapStakingTypeToSubQueryId)
|
||||||
|
export const STAKING_TYPE_RELAYCHAIN = "relaychain";
|
||||||
|
export const STAKING_TYPE_NOMINATION_POOL = "nomination-pool";
|
||||||
|
|
||||||
|
// Substrate default inflation parameters (Kusama-like, no parachains)
|
||||||
|
export const INFLATION_FALLOFF = 0.05;
|
||||||
|
export const INFLATION_MAX = 0.1;
|
||||||
|
export const INFLATION_MIN = 0.025;
|
||||||
|
export const INFLATION_STAKE_TARGET = 0.75;
|
||||||
|
|
||||||
|
// Commission is stored in perbill (1_000_000_000 = 100%)
|
||||||
|
export const PERBILL_DIVISOR = 1_000_000_000;
|
||||||
Reference in New Issue
Block a user