mirror of
https://github.com/pezkuwichain/pezkuwi-telegram-miniapp.git
synced 2026-04-22 03:07:55 +00:00
fix: use ClaimedRewards double-map for accurate unclaimed reward detection
- Replace legacy ledger.claimedRewards (empty in paged-rewards Substrate) with ClaimedRewards(era, validator) storage double-map - Track user's page in erasStakersPaged and check against claimed pages - Use historyDepth instead of hardcoded 10 eras - Split batch payouts into groups of 5 to avoid block weight overflow - Use utility.batch instead of batchAll for resilience - Pass page to payoutStakersByPage when available
This commit is contained in:
+162
-73
@@ -1,14 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* Staking Rewards - Unclaimed reward detection and payout for Asset Hub
|
* Staking Rewards - Unclaimed reward detection and payout for Asset Hub
|
||||||
* Queries on-chain data to find unclaimed era rewards and submits payoutStakers calls
|
* Queries on-chain data to find unclaimed era rewards and submits payoutStakers calls.
|
||||||
|
*
|
||||||
|
* Uses ClaimedRewards(era, validator) double-map storage to accurately detect
|
||||||
|
* which (era, validator, page) combinations have been claimed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ApiPromise } from '@pezkuwi/api';
|
import type { ApiPromise } from '@pezkuwi/api';
|
||||||
import type { KeyringPair } from '@pezkuwi/keyring/types';
|
import type { KeyringPair } from '@pezkuwi/keyring/types';
|
||||||
|
|
||||||
const UNITS = 1_000_000_000_000; // 10^12
|
const UNITS = 1_000_000_000_000; // 10^12
|
||||||
const MAX_ERAS_TO_CHECK = 10;
|
const DEFAULT_HISTORY_DEPTH = 84;
|
||||||
const MAX_PAGES_PER_VALIDATOR = 3;
|
const MAX_PAGES_PER_VALIDATOR = 3;
|
||||||
|
const MAX_BATCH_SIZE = 5; // max payout calls per batch TX
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// TYPES
|
// TYPES
|
||||||
@@ -17,6 +21,7 @@ const MAX_PAGES_PER_VALIDATOR = 3;
|
|||||||
export interface UnclaimedEraReward {
|
export interface UnclaimedEraReward {
|
||||||
era: number;
|
era: number;
|
||||||
validator: string;
|
validator: string;
|
||||||
|
page: number;
|
||||||
estimatedReward: string; // formatted HEZ
|
estimatedReward: string; // formatted HEZ
|
||||||
estimatedRewardRaw: bigint;
|
estimatedRewardRaw: bigint;
|
||||||
}
|
}
|
||||||
@@ -45,7 +50,8 @@ function formatHez(raw: bigint): string {
|
|||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find unclaimed staking rewards for an address on Asset Hub
|
* Find unclaimed staking rewards for an address on Asset Hub.
|
||||||
|
* Checks the ClaimedRewards(era, validator) double-map for accurate detection.
|
||||||
*/
|
*/
|
||||||
export async function getUnclaimedRewards(
|
export async function getUnclaimedRewards(
|
||||||
assetHubApi: ApiPromise,
|
assetHubApi: ApiPromise,
|
||||||
@@ -71,37 +77,55 @@ export async function getUnclaimedRewards(
|
|||||||
// 2. Get ledger - if no ledger, user is not staking
|
// 2. Get ledger - if no ledger, user is not staking
|
||||||
const ledgerOpt = await staking.ledger(address);
|
const ledgerOpt = await staking.ledger(address);
|
||||||
if (!ledgerOpt || ledgerOpt.isNone) return { ...empty, currentEra };
|
if (!ledgerOpt || ledgerOpt.isNone) return { ...empty, currentEra };
|
||||||
const ledgerJson = ledgerOpt.unwrap().toJSON();
|
|
||||||
|
|
||||||
// Get claimed eras from ledger
|
// 3. Get nominated validators (current + historical from exposure)
|
||||||
const claimedEras: number[] = ledgerJson.claimedRewards || ledgerJson.legacyClaimedRewards || [];
|
|
||||||
const claimedSet = new Set(claimedEras);
|
|
||||||
|
|
||||||
// 3. Get nominated validators
|
|
||||||
const nominatorsOpt = await staking.nominators(address);
|
const nominatorsOpt = await staking.nominators(address);
|
||||||
if (!nominatorsOpt || nominatorsOpt.isNone) return { ...empty, currentEra };
|
if (!nominatorsOpt || nominatorsOpt.isNone) return { ...empty, currentEra };
|
||||||
const nominatorsJson = nominatorsOpt.unwrap().toJSON();
|
const nominatorsJson = nominatorsOpt.unwrap().toJSON();
|
||||||
const nominatedValidators: string[] = nominatorsJson.targets || [];
|
const nominatedValidators: string[] = nominatorsJson.targets || [];
|
||||||
if (nominatedValidators.length === 0) return { ...empty, currentEra };
|
if (nominatedValidators.length === 0) return { ...empty, currentEra };
|
||||||
|
|
||||||
// 4. Check last N eras for unclaimed rewards
|
// 4. Determine era range to check
|
||||||
const startEra = Math.max(0, currentEra - 1);
|
let historyDepth = DEFAULT_HISTORY_DEPTH;
|
||||||
const endEra = Math.max(0, currentEra - MAX_ERAS_TO_CHECK);
|
try {
|
||||||
|
if (staking.historyDepth) {
|
||||||
|
const hd = await staking.historyDepth();
|
||||||
|
const hdVal = hd?.toJSON?.() ?? hd?.toNumber?.();
|
||||||
|
if (typeof hdVal === 'number' && hdVal > 0) historyDepth = hdVal;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// historyDepth might be a const, not a storage item
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const consts = (assetHubApi.consts.staking as any)?.historyDepth;
|
||||||
|
if (consts) {
|
||||||
|
const val = consts.toNumber?.() ?? Number(consts.toString());
|
||||||
|
if (val > 0) historyDepth = val;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// use default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startEra = Math.max(0, currentEra - 1); // current era is not yet finalized
|
||||||
|
const endEra = Math.max(0, currentEra - historyDepth);
|
||||||
|
|
||||||
const unclaimed: UnclaimedEraReward[] = [];
|
const unclaimed: UnclaimedEraReward[] = [];
|
||||||
|
|
||||||
// Process eras in parallel batches
|
// Process eras in parallel batches of 5 to avoid overwhelming RPC
|
||||||
const eraPromises: Promise<UnclaimedEraReward[]>[] = [];
|
const PARALLEL_BATCH = 5;
|
||||||
|
for (let batchStart = startEra; batchStart >= endEra; batchStart -= PARALLEL_BATCH) {
|
||||||
|
const batchEnd = Math.max(endEra, batchStart - PARALLEL_BATCH + 1);
|
||||||
|
const eraPromises: Promise<UnclaimedEraReward[]>[] = [];
|
||||||
|
|
||||||
for (let era = startEra; era >= endEra; era--) {
|
for (let era = batchStart; era >= batchEnd; era--) {
|
||||||
if (claimedSet.has(era)) continue;
|
eraPromises.push(checkEraRewards(staking, era, address, nominatedValidators));
|
||||||
|
}
|
||||||
|
|
||||||
eraPromises.push(checkEraRewards(staking, era, address, nominatedValidators));
|
const results = await Promise.all(eraPromises);
|
||||||
}
|
for (const eraResults of results) {
|
||||||
|
unclaimed.push(...eraResults);
|
||||||
const results = await Promise.all(eraPromises);
|
}
|
||||||
for (const eraResults of results) {
|
|
||||||
unclaimed.push(...eraResults);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by era descending
|
// Sort by era descending
|
||||||
@@ -118,9 +142,9 @@ export async function getUnclaimedRewards(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check a single era for unclaimed rewards across all nominated validators
|
* Check a single era for unclaimed rewards across all nominated validators.
|
||||||
|
* Uses ClaimedRewards(era, validator) to determine which pages are already claimed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function checkEraRewards(
|
async function checkEraRewards(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
staking: any,
|
staking: any,
|
||||||
@@ -141,8 +165,26 @@ async function checkEraRewards(
|
|||||||
const totalStake = BigInt(overview.total || '0');
|
const totalStake = BigInt(overview.total || '0');
|
||||||
if (totalStake === 0n) continue;
|
if (totalStake === 0n) continue;
|
||||||
|
|
||||||
// Check if user is in the validator's exposure pages
|
// Get claimed pages for this (era, validator) from ClaimedRewards double-map
|
||||||
|
let claimedPages: number[] = [];
|
||||||
|
try {
|
||||||
|
if (staking.claimedRewards) {
|
||||||
|
const claimedOpt = await staking.claimedRewards(era, validator);
|
||||||
|
if (claimedOpt) {
|
||||||
|
const claimedJson = claimedOpt.toJSON?.() ?? claimedOpt;
|
||||||
|
if (Array.isArray(claimedJson)) {
|
||||||
|
claimedPages = claimedJson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// claimedRewards storage might not exist on older runtimes
|
||||||
|
}
|
||||||
|
const claimedPageSet = new Set(claimedPages);
|
||||||
|
|
||||||
|
// Find user in the validator's exposure pages and check if their page is claimed
|
||||||
let userStake = 0n;
|
let userStake = 0n;
|
||||||
|
let userPage = -1;
|
||||||
const pagesToCheck = Math.min(pageCount, MAX_PAGES_PER_VALIDATOR);
|
const pagesToCheck = Math.min(pageCount, MAX_PAGES_PER_VALIDATOR);
|
||||||
|
|
||||||
for (let page = 0; page < pagesToCheck; page++) {
|
for (let page = 0; page < pagesToCheck; page++) {
|
||||||
@@ -155,13 +197,17 @@ async function checkEraRewards(
|
|||||||
for (const nominator of others) {
|
for (const nominator of others) {
|
||||||
if (nominator.who === address) {
|
if (nominator.who === address) {
|
||||||
userStake = BigInt(nominator.value);
|
userStake = BigInt(nominator.value);
|
||||||
|
userPage = page;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (userStake > 0n) break;
|
if (userStake > 0n) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userStake === 0n) continue;
|
if (userStake === 0n || userPage < 0) continue;
|
||||||
|
|
||||||
|
// Skip if user's page is already claimed
|
||||||
|
if (claimedPageSet.has(userPage)) continue;
|
||||||
|
|
||||||
// Calculate estimated reward
|
// Calculate estimated reward
|
||||||
const reward = await calculateEraReward(staking, era, validator, userStake, totalStake);
|
const reward = await calculateEraReward(staking, era, validator, userStake, totalStake);
|
||||||
@@ -170,6 +216,7 @@ async function checkEraRewards(
|
|||||||
results.push({
|
results.push({
|
||||||
era,
|
era,
|
||||||
validator,
|
validator,
|
||||||
|
page: userPage,
|
||||||
estimatedReward: formatHez(reward),
|
estimatedReward: formatHez(reward),
|
||||||
estimatedRewardRaw: reward,
|
estimatedRewardRaw: reward,
|
||||||
});
|
});
|
||||||
@@ -184,9 +231,8 @@ async function checkEraRewards(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate estimated reward for a nominator in a specific era
|
* Calculate estimated reward for a nominator in a specific era
|
||||||
* Formula: eraReward × (valPoints/totalPoints) × (1 - commission/1e9) × (userStake/totalValStake)
|
* Formula: eraReward * (valPoints/totalPoints) * (1 - commission/1e9) * (userStake/totalValStake)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async function calculateEraReward(
|
async function calculateEraReward(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
staking: any,
|
staking: any,
|
||||||
@@ -238,18 +284,26 @@ async function calculateEraReward(
|
|||||||
// ========================================
|
// ========================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit payoutStakers for a single era+validator
|
* Submit payoutStakersByPage for a single era+validator+page.
|
||||||
|
* Falls back to payoutStakers if payoutStakersByPage is not available.
|
||||||
*/
|
*/
|
||||||
export async function payoutStakingReward(
|
export async function payoutStakingReward(
|
||||||
assetHubApi: ApiPromise,
|
assetHubApi: ApiPromise,
|
||||||
keypair: KeyringPair,
|
keypair: KeyringPair,
|
||||||
validator: string,
|
validator: string,
|
||||||
era: number
|
era: number,
|
||||||
|
page?: number
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const tx = (assetHubApi.tx.staking as any).payoutStakers(validator, era);
|
const stakingTx = assetHubApi.tx.staking as any;
|
||||||
|
|
||||||
|
// Use payoutStakersByPage if available and page is specified
|
||||||
|
const tx =
|
||||||
|
page !== undefined && stakingTx.payoutStakersByPage
|
||||||
|
? stakingTx.payoutStakersByPage(validator, era, page)
|
||||||
|
: stakingTx.payoutStakers(validator, era);
|
||||||
|
|
||||||
tx.signAndSend(
|
tx.signAndSend(
|
||||||
keypair,
|
keypair,
|
||||||
@@ -278,66 +332,101 @@ export async function payoutStakingReward(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Submit payoutStakers for all unclaimed rewards using utility.batchAll
|
* Submit payoutStakers for all unclaimed rewards in batches.
|
||||||
* Falls back to sequential calls if utility pallet is not available
|
* Splits into batches of MAX_BATCH_SIZE to avoid block weight limits.
|
||||||
|
* Uses utility.batch (not batchAll) so one failure doesn't abort the whole batch.
|
||||||
*/
|
*/
|
||||||
export async function payoutAllRewards(
|
export async function payoutAllRewards(
|
||||||
assetHubApi: ApiPromise,
|
assetHubApi: ApiPromise,
|
||||||
keypair: KeyringPair,
|
keypair: KeyringPair,
|
||||||
unclaimed: UnclaimedEraReward[]
|
unclaimed: UnclaimedEraReward[]
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string; completed?: number }> {
|
||||||
if (unclaimed.length === 0) return { success: true };
|
if (unclaimed.length === 0) return { success: true, completed: 0 };
|
||||||
|
|
||||||
// Single reward - no need for batch
|
// Single reward - no need for batch
|
||||||
if (unclaimed.length === 1) {
|
if (unclaimed.length === 1) {
|
||||||
return payoutStakingReward(assetHubApi, keypair, unclaimed[0].validator, unclaimed[0].era);
|
const r = unclaimed[0];
|
||||||
|
const result = await payoutStakingReward(assetHubApi, keypair, r.validator, r.era, r.page);
|
||||||
|
return { ...result, completed: result.success ? 1 : 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try utility.batchAll
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const utilityTx = (assetHubApi.tx as any).utility;
|
const utilityTx = (assetHubApi.tx as any).utility;
|
||||||
if (utilityTx?.batchAll) {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const calls = unclaimed.map((r) =>
|
const stakingTx = assetHubApi.tx.staking as any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(assetHubApi.tx.staking as any).payoutStakers(r.validator, r.era)
|
|
||||||
);
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
let totalCompleted = 0;
|
||||||
try {
|
|
||||||
utilityTx
|
// Split into batches
|
||||||
.batchAll(calls)
|
for (let i = 0; i < unclaimed.length; i += MAX_BATCH_SIZE) {
|
||||||
.signAndSend(
|
const batch = unclaimed.slice(i, i + MAX_BATCH_SIZE);
|
||||||
keypair,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
if (batch.length === 1) {
|
||||||
({ status, dispatchError }: any) => {
|
const r = batch[0];
|
||||||
if (status.isFinalized) {
|
const result = await payoutStakingReward(assetHubApi, keypair, r.validator, r.era, r.page);
|
||||||
if (dispatchError) {
|
if (result.success) {
|
||||||
if (dispatchError.isModule) {
|
totalCompleted++;
|
||||||
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
|
} else {
|
||||||
resolve({ success: false, error: `${decoded.section}.${decoded.name}` });
|
return { success: false, error: result.error, completed: totalCompleted };
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use utility.batch for this chunk
|
||||||
|
if (utilityTx?.batch) {
|
||||||
|
const calls = batch.map((r) =>
|
||||||
|
r.page !== undefined && stakingTx.payoutStakersByPage
|
||||||
|
? stakingTx.payoutStakersByPage(r.validator, r.era, r.page)
|
||||||
|
: stakingTx.payoutStakers(r.validator, r.era)
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await new Promise<{ success: boolean; error?: string }>((resolve) => {
|
||||||
|
try {
|
||||||
|
utilityTx
|
||||||
|
.batch(calls)
|
||||||
|
.signAndSend(
|
||||||
|
keypair,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
({ status, dispatchError }: any) => {
|
||||||
|
if (status.isFinalized) {
|
||||||
|
if (dispatchError) {
|
||||||
|
if (dispatchError.isModule) {
|
||||||
|
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
|
||||||
|
resolve({ success: false, error: `${decoded.section}.${decoded.name}` });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: dispatchError.toString() });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
resolve({ success: false, error: dispatchError.toString() });
|
resolve({ success: true });
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
resolve({ success: true });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
.catch((err: Error) => {
|
||||||
.catch((err: Error) => {
|
resolve({ success: false, error: err.message });
|
||||||
resolve({ success: false, error: err.message });
|
});
|
||||||
});
|
} catch (err) {
|
||||||
} catch (err) {
|
resolve({ success: false, error: err instanceof Error ? err.message : String(err) });
|
||||||
resolve({ success: false, error: err instanceof Error ? err.message : String(err) });
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
totalCompleted += batch.length;
|
||||||
|
} else {
|
||||||
|
return { success: false, error: result.error, completed: totalCompleted };
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
// Fallback: sequential calls
|
||||||
|
for (const r of batch) {
|
||||||
|
const result = await payoutStakingReward(assetHubApi, keypair, r.validator, r.era, r.page);
|
||||||
|
if (result.success) {
|
||||||
|
totalCompleted++;
|
||||||
|
} else {
|
||||||
|
return { success: false, error: result.error, completed: totalCompleted };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: sequential calls
|
return { success: true, completed: totalCompleted };
|
||||||
for (const reward of unclaimed) {
|
|
||||||
const result = await payoutStakingReward(assetHubApi, keypair, reward.validator, reward.era);
|
|
||||||
if (!result.success) return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,14 +300,14 @@ export function RewardsSection() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClaimStakingReward = async (validator: string, era: number) => {
|
const handleClaimStakingReward = async (validator: string, era: number, page?: number) => {
|
||||||
if (!assetHubApi || !keypair) return;
|
if (!assetHubApi || !keypair) return;
|
||||||
setClaimingStakingEra(era);
|
setClaimingStakingEra(era);
|
||||||
setTrackingAnimationText(t('rewards.claimingStakingReward'));
|
setTrackingAnimationText(t('rewards.claimingStakingReward'));
|
||||||
setShowTrackingAnimation(true);
|
setShowTrackingAnimation(true);
|
||||||
hapticImpact('medium');
|
hapticImpact('medium');
|
||||||
try {
|
try {
|
||||||
const result = await payoutStakingReward(assetHubApi, keypair, validator, era);
|
const result = await payoutStakingReward(assetHubApi, keypair, validator, era, page);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
hapticNotification('success');
|
hapticNotification('success');
|
||||||
showAlert(t('rewards.stakingClaimSuccess'));
|
showAlert(t('rewards.stakingClaimSuccess'));
|
||||||
@@ -1145,7 +1145,9 @@ export function RewardsSection() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleClaimStakingReward(reward.validator, reward.era)}
|
onClick={() =>
|
||||||
|
handleClaimStakingReward(reward.validator, reward.era, reward.page)
|
||||||
|
}
|
||||||
disabled={!keypair || claimingStaking || claimingStakingEra !== null}
|
disabled={!keypair || claimingStaking || claimingStakingEra !== null}
|
||||||
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-amber-500/20 text-amber-300 hover:bg-amber-500/30 transition-all disabled:opacity-50 ml-2"
|
className="px-3 py-1.5 rounded-lg text-xs font-medium bg-amber-500/20 text-amber-300 hover:bg-amber-500/30 transition-all disabled:opacity-50 ml-2"
|
||||||
>
|
>
|
||||||
@@ -1176,7 +1178,8 @@ export function RewardsSection() {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleClaimStakingReward(
|
handleClaimStakingReward(
|
||||||
unclaimedRewards.unclaimed[0].validator,
|
unclaimedRewards.unclaimed[0].validator,
|
||||||
unclaimedRewards.unclaimed[0].era
|
unclaimedRewards.unclaimed[0].era,
|
||||||
|
unclaimedRewards.unclaimed[0].page
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={!keypair || claimingStaking || claimingStakingEra !== null}
|
disabled={!keypair || claimingStaking || claimingStakingEra !== null}
|
||||||
|
|||||||
Reference in New Issue
Block a user