From 358f13db50ea1ebf404cf5540bab923ce3e0763f Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Wed, 18 Feb 2026 05:18:44 +0300 Subject: [PATCH] fix: save pool stash accounts with relaychain type for wallet compat The wallet queries activeStakers with the pool's bonded stash address and stakingType="relaychain" (unwrapped from nomination-pool). Derive pool stash accounts from bondedPools and save with correct type. --- src/mappings/PoolStakers.ts | 100 +++++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 37 deletions(-) diff --git a/src/mappings/PoolStakers.ts b/src/mappings/PoolStakers.ts index 78f13d2..e0ea7e7 100644 --- a/src/mappings/PoolStakers.ts +++ b/src/mappings/PoolStakers.ts @@ -3,53 +3,78 @@ import { ActiveStaker } from "../types"; import { Option } from "@pezkuwi/types"; import { PEZKUWI_ASSET_HUB_GENESIS, - STAKING_TYPE_NOMINATION_POOL, + STAKING_TYPE_RELAYCHAIN, } from "./constants"; -let poolMembersInitialized = false; +let poolStakersInitialized = false; + +/** + * Derive the bonded (stash) account for a nomination pool. + * Formula: PalletId("py/nopls") + encode((AccountType::Bonded=0, poolId)) padded to 32 bytes + * This matches Substrate's PalletId::into_sub_account_truncating + */ +function derivePoolStash(poolId: number): string { + const buf = new Uint8Array(32); + // PalletId: "py/nopls" (8 bytes) + const palletId = [0x70, 0x79, 0x2f, 0x6e, 0x6f, 0x70, 0x6c, 0x73]; + for (let i = 0; i < 8; i++) buf[i] = palletId[i]; + // AccountType::Bonded = 0 + buf[8] = 0; + // Pool ID as u32 LE + buf[9] = poolId & 0xff; + buf[10] = (poolId >> 8) & 0xff; + buf[11] = (poolId >> 16) & 0xff; + buf[12] = (poolId >> 24) & 0xff; + // Remaining bytes are already 0 (padding) + return api.registry.createType("AccountId", buf).toString(); +} /** * Block handler: on the FIRST block processed, query the live chain state - * for all current nomination pool members and save them as ActiveStakers. - * This ensures existing pool members are captured even if their Bonded - * events were in pruned blocks. + * for all bonded pools and save their stash accounts as ActiveStakers. + * + * The wallet queries activeStakers with: + * - address: pool stash (bonded) account + * - stakingType: "relaychain" (unwrapped from nomination-pool) + * - networkId: AH genesis */ export async function handleBlock(block: SubstrateBlock): Promise { - if (poolMembersInitialized) return; - poolMembersInitialized = true; + if (poolStakersInitialized) return; + poolStakersInitialized = true; - logger.info("Initializing active pool stakers from live chain state..."); + logger.info("Initializing pool stash accounts from live chain state..."); - const members = await api.query.nominationPools.poolMembers.entries(); + const pools = await api.query.nominationPools.bondedPools.entries(); let count = 0; - for (const [key, memberOpt] of members) { - const member = (memberOpt as Option); - if (member.isNone) continue; + for (const [key, poolOpt] of pools) { + const pool = poolOpt as Option; + if (pool.isNone) continue; - const unwrapped = member.unwrap(); + const unwrapped = pool.unwrap(); if (unwrapped.points.toBigInt() === BigInt(0)) continue; - const address = key.args[0].toString(); - const stakerId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_NOMINATION_POOL}-${address}`; + const poolId = (key.args[0] as any).toNumber(); + const stashAddress = derivePoolStash(poolId); + const stakerId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_RELAYCHAIN}-${stashAddress}`; const staker = ActiveStaker.create({ id: stakerId, networkId: PEZKUWI_ASSET_HUB_GENESIS, - stakingType: STAKING_TYPE_NOMINATION_POOL, - address, + stakingType: STAKING_TYPE_RELAYCHAIN, + address: stashAddress, }); await staker.save(); count++; } - logger.info(`Initialized ${count} active pool stakers from chain state`); + logger.info(`Initialized ${count} pool stash accounts as active stakers`); } /** * Handle nominationPools.Bonded event - * Fired when a member bonds (joins or adds more) to a nomination pool. - * Creates an ActiveStaker entry for this address. + * When a member bonds to a pool, ensure the pool's stash account is + * saved as an ActiveStaker with relaychain type. * * Event data: [member: AccountId, pool_id: u32, bonded: Balance, joined: bool] */ @@ -58,28 +83,29 @@ export async function handlePoolBonded( ): Promise { const { event: { - data: [memberEncoded], + data: [, poolIdEncoded], }, } = event; - const address = memberEncoded.toString(); + const poolId = (poolIdEncoded as any).toNumber(); + const stashAddress = derivePoolStash(poolId); - const stakerId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_NOMINATION_POOL}-${address}`; + const stakerId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_RELAYCHAIN}-${stashAddress}`; const staker = ActiveStaker.create({ id: stakerId, networkId: PEZKUWI_ASSET_HUB_GENESIS, - stakingType: STAKING_TYPE_NOMINATION_POOL, - address, + stakingType: STAKING_TYPE_RELAYCHAIN, + address: stashAddress, }); await staker.save(); - logger.info(`Pool staker added: ${address}`); + logger.info(`Pool ${poolId} stash saved: ${stashAddress}`); } /** * Handle nominationPools.Unbonded event - * Fired when a member unbonds from a nomination pool. - * If the member has no remaining points, remove the ActiveStaker entry. + * If the pool has no remaining points after unbond, remove the stash + * from ActiveStakers. * * Event data: [member: AccountId, pool_id: u32, balance: Balance, points: Balance, era: u32] */ @@ -88,21 +114,21 @@ export async function handlePoolUnbonded( ): Promise { const { event: { - data: [memberEncoded], + data: [, poolIdEncoded], }, } = event; - const address = memberEncoded.toString(); + const poolId = (poolIdEncoded as any).toNumber(); - // Check if member still has points in the pool - const memberData = (await api.query.nominationPools.poolMembers( - address, + // Check if pool still has points + const poolData = (await api.query.nominationPools.bondedPools( + poolId, )) as Option; - if (memberData.isNone || memberData.unwrap().points.toBigInt() === BigInt(0)) { - // Member fully left the pool - remove active staker - const stakerId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_NOMINATION_POOL}-${address}`; + if (poolData.isNone || poolData.unwrap().points.toBigInt() === BigInt(0)) { + const stashAddress = derivePoolStash(poolId); + const stakerId = `${PEZKUWI_ASSET_HUB_GENESIS}-${STAKING_TYPE_RELAYCHAIN}-${stashAddress}`; await ActiveStaker.remove(stakerId); - logger.info(`Pool staker removed: ${address}`); + logger.info(`Pool ${poolId} stash removed: ${stashAddress}`); } }