diff --git a/src/services/accounts/AccountsStakingPayoutsService.spec.ts b/src/services/accounts/AccountsStakingPayoutsService.spec.ts index 6389e3c15..4fc83f0f1 100644 --- a/src/services/accounts/AccountsStakingPayoutsService.spec.ts +++ b/src/services/accounts/AccountsStakingPayoutsService.spec.ts @@ -581,4 +581,75 @@ describe('AccountsStakingPayoutsService', () => { expect(Object.keys(res.validators).length).toEqual(3); }); }); + describe('ClaimedRewards storage path (post-AHM)', () => { + // Mock historicApi with claimedRewards that returns claimed pages + const mockHistoricApiWithClaimedRewards = (pages: number[]) => + ({ + ...mockHistoricApi, + query: { + ...mockHistoricApi.query, + staking: { + ...mockHistoricApi.query.staking, + claimedRewards: (_era: unknown, _validator: unknown) => + Promise.resolve(polkadotRegistryV1000001.createType('Vec', pages)), + }, + }, + }) as unknown as ApiDecoration<'promise'>; + + it('should filter out claimed payouts when unclaimedOnly=true and ClaimedRewards is non-empty', async () => { + const res = await stakingPayoutsService.fetchAccountStakingPayout( + blockHash, + validator, + 1, + ERA, + true, // unclaimedOnly + ERA + 1, + mockHistoricApiWithClaimedRewards([0]), + ); + + const erasPayouts = res.erasPayouts[0]; + expect('payouts' in erasPayouts).toBe(true); + if ('payouts' in erasPayouts) { + expect(erasPayouts.payouts).toStrictEqual([]); + } + }); + + it('should show payouts as unclaimed when ClaimedRewards is empty', async () => { + const res = await stakingPayoutsService.fetchAccountStakingPayout( + blockHash, + validator, + 1, + ERA, + true, // unclaimedOnly + ERA + 1, + mockHistoricApiWithClaimedRewards([]), + ); + + const erasPayouts = res.erasPayouts[0]; + expect('payouts' in erasPayouts).toBe(true); + if ('payouts' in erasPayouts) { + expect(erasPayouts.payouts.length).toBeGreaterThan(0); + expect(erasPayouts.payouts[0].claimed).toBe(false); + } + }); + + it('should return claimed=true when ClaimedRewards is non-empty and unclaimedOnly=false', async () => { + const res = await stakingPayoutsService.fetchAccountStakingPayout( + blockHash, + validator, + 1, + ERA, + false, // unclaimedOnly + ERA + 1, + mockHistoricApiWithClaimedRewards([0]), + ); + + const erasPayouts = res.erasPayouts[0]; + expect('payouts' in erasPayouts).toBe(true); + if ('payouts' in erasPayouts) { + expect(erasPayouts.payouts.length).toBeGreaterThan(0); + expect(erasPayouts.payouts[0].claimed).toBe(true); + } + }); + }); }); diff --git a/src/services/accounts/AccountsStakingPayoutsService.ts b/src/services/accounts/AccountsStakingPayoutsService.ts index 7c291b956..d508f1352 100644 --- a/src/services/accounts/AccountsStakingPayoutsService.ts +++ b/src/services/accounts/AccountsStakingPayoutsService.ts @@ -266,7 +266,9 @@ export class AccountsStakingPayoutsService extends AbstractService { return { at, - erasPayouts: allEraData.map((eraData) => this.deriveEraPayouts(address, unclaimedOnly, eraData, isKusama)), + erasPayouts: await Promise.all( + allEraData.map((eraData) => this.deriveEraPayouts(address, unclaimedOnly, eraData, isKusama, historicApi)), + ), }; } @@ -546,12 +548,13 @@ export class AccountsStakingPayoutsService extends AbstractService { * @param era the era to query * @param eraData data about the address and era we are calculating payouts for */ - deriveEraPayouts( + async deriveEraPayouts( address: string, unclaimedOnly: boolean, { deriveEraExposure, eraRewardPoints, erasValidatorRewardOption, exposuresWithCommission, eraIndex }: IEraData, isKusama: boolean, - ): IEraPayouts | { message: string } { + historicApi: ApiDecoration<'promise'>, + ): Promise { if (!exposuresWithCommission) { return { message: `${address} has no nominations for the era ${eraIndex.toString()}`, @@ -603,28 +606,41 @@ export class AccountsStakingPayoutsService extends AbstractService { /** * Check if the reward has already been claimed. * - * It is important to note that the following examines types that are both current and historic. - * When going back far enough in certain chains types such as `StakingLedgerTo240` are necessary for grabbing - * any reward data. */ - let indexOfEra: number; - if (validatorLedger.legacyClaimedRewards) { - indexOfEra = validatorLedger.legacyClaimedRewards.indexOf(eraIndex); - } else if ((validatorLedger as unknown as StakingLedger).claimedRewards) { - indexOfEra = (validatorLedger as unknown as StakingLedger).claimedRewards.indexOf(eraIndex); - } else if ((validatorLedger as unknown as StakingLedgerTo240).lastReward) { - const lastReward = (validatorLedger as unknown as StakingLedgerTo240).lastReward; - if (lastReward.isSome) { - indexOfEra = lastReward.unwrap().toNumber(); + let claimed = false; + const claimedRewardsQuery = + historicApi.query.staking?.claimedRewards ?? historicApi.query.staking?.erasClaimedRewards; + + if (claimedRewardsQuery) { + const claimedPages = await claimedRewardsQuery>(eraIndex, validatorId); + if (claimedPages && claimedPages.length > 0) { + const overview = deriveEraExposure.validatorsOverview?.[validatorId]; + if (overview && overview.isSome) { + claimed = claimedPages.length >= overview.unwrap().pageCount.toNumber(); + } else { + claimed = true; + } + } + } else { + let indexOfEra: number; + if (validatorLedger.legacyClaimedRewards) { + indexOfEra = validatorLedger.legacyClaimedRewards.indexOf(eraIndex); + } else if ((validatorLedger as unknown as StakingLedger).claimedRewards) { + indexOfEra = (validatorLedger as unknown as StakingLedger).claimedRewards.indexOf(eraIndex); + } else if ((validatorLedger as unknown as StakingLedgerTo240).lastReward) { + const lastReward = (validatorLedger as unknown as StakingLedgerTo240).lastReward; + if (lastReward.isSome) { + indexOfEra = lastReward.unwrap().toNumber(); + } else { + indexOfEra = -1; + } + } else if (eraIndex.toNumber() < 518 && isKusama) { + indexOfEra = eraIndex.toNumber(); } else { indexOfEra = -1; } - } else if (eraIndex.toNumber() < 518 && isKusama) { - indexOfEra = eraIndex.toNumber(); - } else { - indexOfEra = -1; + claimed = Number.isInteger(indexOfEra) && indexOfEra !== -1; } - const claimed: boolean = Number.isInteger(indexOfEra) && indexOfEra !== -1; if (unclaimedOnly && claimed) { continue; } @@ -1024,9 +1040,10 @@ export class AccountsStakingPayoutsService extends AbstractService { }, ); - return allEraData - .map((eraData) => this.deriveEraPayouts(address, unclaimedOnly, eraData, isKusama)) - .filter((payout): payout is IEraPayouts => !('message' in payout)); + const eraPayouts = await Promise.all( + allEraData.map((eraData) => this.deriveEraPayouts(address, unclaimedOnly, eraData, isKusama, historicRelayApi)), + ); + return eraPayouts.filter((payout): payout is IEraPayouts => !('message' in payout)); } /** @@ -1097,8 +1114,9 @@ export class AccountsStakingPayoutsService extends AbstractService { }, ); - return allEraData - .map((eraData) => this.deriveEraPayouts(address, unclaimedOnly, eraData, isKusama)) - .filter((payout): payout is IEraPayouts => !('message' in payout)); + const eraPayouts = await Promise.all( + allEraData.map((eraData) => this.deriveEraPayouts(address, unclaimedOnly, eraData, isKusama, historicApi)), + ); + return eraPayouts.filter((payout): payout is IEraPayouts => !('message' in payout)); } }