From e4abee939fe3c0d3be687fb590478f2a69722567 Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Sat, 21 Feb 2026 02:55:27 +0300 Subject: [PATCH] feat: migrate staking from Relay Chain to Asset Hub - shared/staking.ts: update for AH (remove babe dep, remove validatorPool, bonding 2 eras) - StakingDashboard: switch all staking operations from api (RC) to assetHubApi (AH) --- shared/lib/staking.ts | 103 ++++++++---------- .../components/staking/StakingDashboard.tsx | 64 +++++------ 2 files changed, 80 insertions(+), 87 deletions(-) diff --git a/shared/lib/staking.ts b/shared/lib/staking.ts index d2f1993e..be32d6e9 100644 --- a/shared/lib/staking.ts +++ b/shared/lib/staking.ts @@ -1,7 +1,9 @@ // ======================================== -// Staking Helper Functions +// Staking Helper Functions (Asset Hub) // ======================================== -// Helper functions for pallet_staking and pallet_staking_score integration +// Helper functions for pallet_staking_async on Asset Hub and pallet_staking_score on People Chain. +// Staking was moved from Relay Chain to Asset Hub. +// The `api` parameter in all functions refers to the Asset Hub API connection. import { ApiPromise } from '@pezkuwi/api'; import { formatBalance } from './wallet'; @@ -143,7 +145,9 @@ export async function getCurrentEra(api: ApiPromise): Promise { } /** - * Get blocks remaining until an era + * Get estimated time remaining until an era (in seconds). + * Asset Hub uses Aura (no babe.epochDuration), so we estimate based on + * activeEra.start timestamp + sessionsPerEra * estimated session duration. */ export async function getBlocksUntilEra( api: ApiPromise, @@ -155,26 +159,33 @@ export async function getBlocksUntilEra( return 0; } + const erasRemaining = targetEra - currentEra; + + // Try to get sessionsPerEra from staking constants + const sessionsPerEra = api.consts.staking?.sessionsPerEra + ? Number(api.consts.staking.sessionsPerEra.toString()) + : 6; // Default: 6 sessions per era on AH + + // Estimate session duration: ~1 hour (3600 seconds / 6s block time = 600 blocks) + const ESTIMATED_SESSION_BLOCKS = 600; + const blocksPerEra = sessionsPerEra * ESTIMATED_SESSION_BLOCKS; + + // Try to estimate blocks remaining in current era using activeEra.start const activeEraOption = await api.query.staking.activeEra(); - if (activeEraOption.isNone) { - return 0; + if (activeEraOption.isSome) { + const activeEra = activeEraOption.unwrap() as { start: { unwrapOr: (def: number) => { toString: () => string } } }; + const eraStartSlot = Number(activeEra.start.unwrapOr(0).toString()); + + if (eraStartSlot > 0) { + const currentBlock = Number((await api.query.system.number()).toString()); + const blocksIntoCurrentEra = currentBlock - eraStartSlot; + const blocksRemainingInCurrentEra = Math.max(0, blocksPerEra - blocksIntoCurrentEra); + return blocksRemainingInCurrentEra + (blocksPerEra * (erasRemaining - 1)); + } } - const activeEra = activeEraOption.unwrap() as { start: { unwrapOr: (def: number) => { toString: () => string } } }; - const eraStartBlock = Number(activeEra.start.unwrapOr(0).toString()); - - // Get session length and sessions per era - const sessionLength = api.consts.babe?.epochDuration || api.consts.timestamp?.minimumPeriod || 600; - const sessionsPerEra = api.consts.staking.sessionsPerEra; - - const blocksPerEra = Number(sessionLength.toString()) * Number(sessionsPerEra.toString()); - const currentBlock = Number((await api.query.system.number()).toString()); - - const erasRemaining = targetEra - currentEra; - const blocksIntoCurrentEra = currentBlock - eraStartBlock; - const blocksRemainingInCurrentEra = blocksPerEra - blocksIntoCurrentEra; - - return blocksRemainingInCurrentEra + (blocksPerEra * (erasRemaining - 1)); + // Fallback: just multiply eras remaining by estimated blocks per era + return blocksPerEra * erasRemaining; } catch (error) { console.error('Error calculating blocks until era:', error); return 0; @@ -183,7 +194,7 @@ export async function getBlocksUntilEra( /** * Get comprehensive staking info for an account - * @param api - Relay Chain API (for staking pallet) + * @param api - Asset Hub API (staking pallet moved from RC to AH) * @param address - User address * @param peopleApi - Optional People Chain API (for pezRewards and stakingScore pallets) */ @@ -356,34 +367,12 @@ export async function getStakingInfo( } /** - * Get list of active validators - * For Pezkuwi, we query staking.validators.entries() to get all registered validators + * Get list of active validators from Asset Hub staking pallet. + * Note: validatorPool pallet is on Relay Chain, not AH. We only use staking.validators here. */ export async function getActiveValidators(api: ApiPromise): Promise { try { - // Try multiple methods to get validators - - // Method 1: Try validatorPool.currentValidatorSet() if available - if (api.query.validatorPool && api.query.validatorPool.currentValidatorSet) { - try { - const currentSetOption = await api.query.validatorPool.currentValidatorSet(); - if (currentSetOption.isSome) { - const validatorSet = currentSetOption.unwrap() as any; - // Extract validators array from the set structure - if (validatorSet.validators && Array.isArray(validatorSet.validators)) { - const validators = validatorSet.validators.map((v: any) => v.toString()); - if (validators.length > 0) { - console.log(`Found ${validators.length} validators from validatorPool.currentValidatorSet`); - return validators; - } - } - } - } catch (err) { - console.warn('validatorPool.currentValidatorSet query failed:', err); - } - } - - // Method 2: Query staking.validators.entries() to get all registered validators + // Method 1: Query staking.validators.entries() on Asset Hub try { const validatorEntries = await api.query.staking.validators.entries(); if (validatorEntries.length > 0) { @@ -395,14 +384,18 @@ export async function getActiveValidators(api: ApiPromise): Promise { console.warn('staking.validators.entries() query failed:', err); } - // Method 3: Fallback to session.validators() - const sessionValidators = await api.query.session.validators(); - const validatorArray = Array.isArray(sessionValidators) - ? sessionValidators - : (sessionValidators as unknown as { toJSON: () => string[] }).toJSON(); - const validators = validatorArray.map((v: unknown) => String(v)); - console.log(`Found ${validators.length} validators from session.validators()`); - return validators; + // Method 2: Fallback to session.validators() if available on AH + if (api.query.session?.validators) { + const sessionValidators = await api.query.session.validators(); + const validatorArray = Array.isArray(sessionValidators) + ? sessionValidators + : (sessionValidators as unknown as { toJSON: () => string[] }).toJSON(); + const validators = validatorArray.map((v: unknown) => String(v)); + console.log(`Found ${validators.length} validators from session.validators()`); + return validators; + } + + return []; } catch (error) { console.error('Error fetching validators:', error); return []; @@ -431,7 +424,7 @@ export async function getBondingDuration(api: ApiPromise): Promise { return Number(duration.toString()); } catch (error) { console.error('Error fetching bonding duration:', error); - return 28; // Default 28 eras + return 2; // Default 2 eras (AH bonding duration) } } diff --git a/web/src/components/staking/StakingDashboard.tsx b/web/src/components/staking/StakingDashboard.tsx index a77de839..54f9d3bb 100644 --- a/web/src/components/staking/StakingDashboard.tsx +++ b/web/src/components/staking/StakingDashboard.tsx @@ -44,7 +44,7 @@ async function getInjectorSigner(address: string) { } export const StakingDashboard: React.FC = () => { - const { api, peopleApi, selectedAccount, isApiReady, isPeopleReady } = usePezkuwi(); + const { assetHubApi, peopleApi, selectedAccount, isAssetHubReady, isPeopleReady } = usePezkuwi(); const { balances, refreshBalances } = useWallet(); const [stakingInfo, setStakingInfo] = useState(null); @@ -62,21 +62,21 @@ export const StakingDashboard: React.FC = () => { const [isRecordingScore, setIsRecordingScore] = useState(false); const [isClaimingReward, setIsClaimingReward] = useState(false); - // Fetch staking data + // Fetch staking data from Asset Hub useEffect(() => { const fetchStakingData = async () => { - if (!api || !isApiReady || !selectedAccount) { + if (!assetHubApi || !isAssetHubReady || !selectedAccount) { return; } setIsLoadingData(true); try { const [info, activeVals, minBond, duration, era] = await Promise.all([ - getStakingInfo(api, selectedAccount.address, peopleApi || undefined), - getActiveValidators(api), - getMinNominatorBond(api), - getBondingDuration(api), - getCurrentEra(api) + getStakingInfo(assetHubApi, selectedAccount.address, peopleApi || undefined), + getActiveValidators(assetHubApi), + getMinNominatorBond(assetHubApi), + getBondingDuration(assetHubApi), + getCurrentEra(assetHubApi) ]); setStakingInfo(info); @@ -101,7 +101,7 @@ export const StakingDashboard: React.FC = () => { fetchStakingData(); const interval = setInterval(fetchStakingData, 30000); // Refresh every 30s return () => clearInterval(interval); - }, [api, peopleApi, isApiReady, isPeopleReady, selectedAccount]); + }, [assetHubApi, peopleApi, isAssetHubReady, isPeopleReady, selectedAccount]); // Fetch PEZ rewards data separately from People Chain useEffect(() => { @@ -180,7 +180,7 @@ export const StakingDashboard: React.FC = () => { }; const handleBond = async () => { - if (!api || !selectedAccount || !bondAmount) return; + if (!assetHubApi || !selectedAccount || !bondAmount) return; setIsLoading(true); try { @@ -200,10 +200,10 @@ export const StakingDashboard: React.FC = () => { // If already bonded, use bondExtra, otherwise use bond let tx; if (stakingInfo && parseFloat(stakingInfo.bonded) > 0) { - tx = api.tx.staking.bondExtra(amount); + tx = assetHubApi.tx.staking.bondExtra(amount); } else { // For new bond, also need to specify reward destination - tx = api.tx.staking.bond(amount, 'Staked'); // Auto-compound rewards + tx = assetHubApi.tx.staking.bond(amount, 'Staked'); // Auto-compound rewards } await tx.signAndSend( @@ -214,7 +214,7 @@ export const StakingDashboard: React.FC = () => { if (import.meta.env.DEV) console.log('Transaction in block:', status.asInBlock.toHex()); if (dispatchError) { - handleBlockchainError(dispatchError, api, toast); + handleBlockchainError(dispatchError, assetHubApi, toast); setIsLoading(false); } else { handleBlockchainSuccess('staking.bonded', toast, { amount: bondAmount }); @@ -222,8 +222,8 @@ export const StakingDashboard: React.FC = () => { refreshBalances(); // Refresh staking data after a delay setTimeout(() => { - if (api && selectedAccount) { - getStakingInfo(api, selectedAccount.address, peopleApi || undefined).then(setStakingInfo); + if (assetHubApi && selectedAccount) { + getStakingInfo(assetHubApi, selectedAccount.address, peopleApi || undefined).then(setStakingInfo); } }, 3000); setIsLoading(false); @@ -239,7 +239,7 @@ export const StakingDashboard: React.FC = () => { }; const handleNominate = async () => { - if (!api || !selectedAccount || selectedValidators.length === 0) return; + if (!assetHubApi || !selectedAccount || selectedValidators.length === 0) return; if (!stakingInfo || parseFloat(stakingInfo.bonded) === 0) { toast.error('You must bond tokens before nominating validators'); @@ -250,7 +250,7 @@ export const StakingDashboard: React.FC = () => { try { const injector = await getInjectorSigner(selectedAccount.address); - const tx = api.tx.staking.nominate(selectedValidators); + const tx = assetHubApi.tx.staking.nominate(selectedValidators); await tx.signAndSend( selectedAccount.address, @@ -258,14 +258,14 @@ export const StakingDashboard: React.FC = () => { ({ status, dispatchError }) => { if (status.isInBlock) { if (dispatchError) { - handleBlockchainError(dispatchError, api, toast); + handleBlockchainError(dispatchError, assetHubApi, toast); setIsLoading(false); } else { handleBlockchainSuccess('staking.nominated', toast, { count: selectedValidators.length.toString() }); // Refresh staking data setTimeout(() => { - if (api && selectedAccount) { - getStakingInfo(api, selectedAccount.address, peopleApi || undefined).then(setStakingInfo); + if (assetHubApi && selectedAccount) { + getStakingInfo(assetHubApi, selectedAccount.address, peopleApi || undefined).then(setStakingInfo); } }, 3000); setIsLoading(false); @@ -281,7 +281,7 @@ export const StakingDashboard: React.FC = () => { }; const handleUnbond = async () => { - if (!api || !selectedAccount || !unbondAmount) return; + if (!assetHubApi || !selectedAccount || !unbondAmount) return; setIsLoading(true); try { @@ -292,7 +292,7 @@ export const StakingDashboard: React.FC = () => { } const injector = await getInjectorSigner(selectedAccount.address); - const tx = api.tx.staking.unbond(amount); + const tx = assetHubApi.tx.staking.unbond(amount); await tx.signAndSend( selectedAccount.address, @@ -300,7 +300,7 @@ export const StakingDashboard: React.FC = () => { ({ status, dispatchError }) => { if (status.isInBlock) { if (dispatchError) { - handleBlockchainError(dispatchError, api, toast); + handleBlockchainError(dispatchError, assetHubApi, toast); setIsLoading(false); } else { handleBlockchainSuccess('staking.unbonded', toast, { @@ -309,8 +309,8 @@ export const StakingDashboard: React.FC = () => { }); setUnbondAmount(''); setTimeout(() => { - if (api && selectedAccount) { - getStakingInfo(api, selectedAccount.address, peopleApi || undefined).then(setStakingInfo); + if (assetHubApi && selectedAccount) { + getStakingInfo(assetHubApi, selectedAccount.address, peopleApi || undefined).then(setStakingInfo); } }, 3000); setIsLoading(false); @@ -326,7 +326,7 @@ export const StakingDashboard: React.FC = () => { }; const handleWithdrawUnbonded = async () => { - if (!api || !selectedAccount) return; + if (!assetHubApi || !selectedAccount) return; if (!stakingInfo || parseFloat(stakingInfo.redeemable) === 0) { toast.info('No tokens available to withdraw'); @@ -338,7 +338,7 @@ export const StakingDashboard: React.FC = () => { const injector = await getInjectorSigner(selectedAccount.address); // Number of slashing spans (usually 0) - const tx = api.tx.staking.withdrawUnbonded(0); + const tx = assetHubApi.tx.staking.withdrawUnbonded(0); await tx.signAndSend( selectedAccount.address, @@ -348,7 +348,7 @@ export const StakingDashboard: React.FC = () => { if (dispatchError) { let errorMessage = 'Withdrawal failed'; if (dispatchError.isModule) { - const decoded = api.registry.findMetaError(dispatchError.asModule); + const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule); errorMessage = `${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`; } toast.error(errorMessage); @@ -357,8 +357,8 @@ export const StakingDashboard: React.FC = () => { toast.success(`Withdrew ${stakingInfo.redeemable} HEZ`); refreshBalances(); setTimeout(() => { - if (api && selectedAccount) { - getStakingInfo(api, selectedAccount.address, peopleApi || undefined).then(setStakingInfo); + if (assetHubApi && selectedAccount) { + getStakingInfo(assetHubApi, selectedAccount.address, peopleApi || undefined).then(setStakingInfo); } }, 3000); setIsLoading(false); @@ -399,8 +399,8 @@ export const StakingDashboard: React.FC = () => { toast.success('Score tracking started successfully! Your staking score will now accumulate over time.'); // Refresh staking data after a delay setTimeout(() => { - if (api && selectedAccount) { - getStakingInfo(api, selectedAccount.address, peopleApi || undefined).then(setStakingInfo); + if (assetHubApi && selectedAccount) { + getStakingInfo(assetHubApi, selectedAccount.address, peopleApi || undefined).then(setStakingInfo); } }, 3000); setIsLoading(false);