Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/services/accounts/AccountsStakingPayoutsService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<u32>', 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);
}
});
});
});
70 changes: 44 additions & 26 deletions src/services/accounts/AccountsStakingPayoutsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
),
};
}

Expand Down Expand Up @@ -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<IEraPayouts | { message: string }> {
if (!exposuresWithCommission) {
return {
message: `${address} has no nominations for the era ${eraIndex.toString()}`,
Expand Down Expand Up @@ -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<Vec<u32>>(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;
}
Expand Down Expand Up @@ -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));
}

/**
Expand Down Expand Up @@ -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));
}
}
Loading