Revert "feat: add LP staking modal and reward pools creation script"

This reverts commit 1d77be9a27.
This commit is contained in:
2026-02-06 14:47:08 +03:00
parent 1d77be9a27
commit d5b4e3b711
3 changed files with 2 additions and 620 deletions
-474
View File
@@ -1,474 +0,0 @@
import React, { useState, useEffect } from 'react';
import { X, Lock, Unlock, Gift, AlertCircle, Loader2, Info } from 'lucide-react';
import { web3FromAddress } from '@pezkuwi/extension-dapp';
import { usePezkuwi } from '@/contexts/PezkuwiContext';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
interface StakingPool {
poolId: number;
stakedAsset: string;
rewardAsset: string;
rewardRatePerBlock: string;
totalStaked: string;
userStaked: string;
pendingRewards: string;
lpTokenId: number;
lpBalance: string;
}
interface LPStakingModalProps {
isOpen: boolean;
onClose: () => void;
}
const LP_TOKEN_NAMES: Record<number, string> = {
0: 'HEZ-PEZ LP',
1: 'HEZ-USDT LP',
2: 'HEZ-DOT LP',
};
export const LPStakingModal: React.FC<LPStakingModalProps> = ({ isOpen, onClose }) => {
const { assetHubApi, selectedAccount, isAssetHubReady } = usePezkuwi();
const [pools, setPools] = useState<StakingPool[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isProcessing, setIsProcessing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [selectedPool, setSelectedPool] = useState<number | null>(null);
const [stakeAmount, setStakeAmount] = useState('');
const [unstakeAmount, setUnstakeAmount] = useState('');
const [activeTab, setActiveTab] = useState('stake');
// Fetch staking pools
useEffect(() => {
if (!assetHubApi || !isAssetHubReady || !isOpen) return;
const fetchPools = async () => {
setIsLoading(true);
try {
const poolEntries = await assetHubApi.query.assetRewards.pools.entries();
const stakingPools: StakingPool[] = [];
for (const [key, value] of poolEntries) {
const poolId = parseInt(key.args[0].toString());
const poolData = value.toJSON() as {
stakedAssetId: { interior: { x2: [{ palletInstance: number }, { generalIndex: number }] } };
rewardAssetId: { interior: { x2: [{ palletInstance: number }, { generalIndex: number }] } };
rewardRatePerBlock: string;
totalTokensStaked: string;
lastUpdatedBlock: number;
};
// Extract LP token ID from staked asset location
const lpTokenId = poolData.stakedAssetId?.interior?.x2?.[1]?.generalIndex ?? 0;
// Get user's stake if account connected
let userStaked = '0';
const pendingRewards = '0'; // TODO: Calculate from reward debt
let lpBalance = '0';
if (selectedAccount) {
try {
const stakeInfo = await assetHubApi.query.assetRewards.poolStakers([poolId, selectedAccount.address]);
if (stakeInfo && (stakeInfo as { isSome: boolean }).isSome) {
const stakeData = (stakeInfo as { unwrap: () => { toJSON: () => { amount: string; rewardDebt: string } } }).unwrap().toJSON();
userStaked = stakeData.amount || '0';
// Pending rewards calculation would need more complex logic
}
// Get LP token balance from poolAssets
const lpBal = await assetHubApi.query.poolAssets.account(lpTokenId, selectedAccount.address);
if (lpBal && (lpBal as { isSome: boolean }).isSome) {
const lpData = (lpBal as { unwrap: () => { toJSON: () => { balance: string } } }).unwrap().toJSON();
lpBalance = lpData.balance || '0';
}
} catch (e) {
console.error('Error fetching user stake:', e);
}
}
stakingPools.push({
poolId,
stakedAsset: LP_TOKEN_NAMES[lpTokenId] || `LP Token #${lpTokenId}`,
rewardAsset: 'PEZ',
rewardRatePerBlock: poolData.rewardRatePerBlock || '0',
totalStaked: poolData.totalTokensStaked || '0',
userStaked,
pendingRewards,
lpTokenId,
lpBalance,
});
}
setPools(stakingPools);
if (stakingPools.length > 0 && selectedPool === null) {
setSelectedPool(stakingPools[0].poolId);
}
} catch (err) {
console.error('Error fetching staking pools:', err);
setError('Failed to fetch staking pools');
} finally {
setIsLoading(false);
}
};
fetchPools();
}, [assetHubApi, isAssetHubReady, isOpen, selectedAccount, selectedPool]);
const formatAmount = (amount: string, decimals: number = 12): string => {
const value = Number(amount) / Math.pow(10, decimals);
return value.toLocaleString(undefined, { maximumFractionDigits: 4 });
};
const handleStake = async () => {
if (!assetHubApi || !selectedAccount || selectedPool === null || !stakeAmount) return;
setIsProcessing(true);
setError(null);
setSuccess(null);
try {
const pool = pools.find(p => p.poolId === selectedPool);
if (!pool) throw new Error('Pool not found');
const amountBN = BigInt(Math.floor(parseFloat(stakeAmount) * 1e12));
const injector = await web3FromAddress(selectedAccount.address);
const tx = assetHubApi.tx.assetRewards.stake(selectedPool, amountBN.toString());
await new Promise<void>((resolve, reject) => {
tx.signAndSend(
selectedAccount.address,
{ signer: injector.signer },
({ status, dispatchError }) => {
if (status.isFinalized) {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`));
} else {
reject(new Error(dispatchError.toString()));
}
} else {
resolve();
}
}
}
);
});
setSuccess(`Successfully staked ${stakeAmount} ${pool.stakedAsset}!`);
setStakeAmount('');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to stake');
} finally {
setIsProcessing(false);
}
};
const handleUnstake = async () => {
if (!assetHubApi || !selectedAccount || selectedPool === null || !unstakeAmount) return;
setIsProcessing(true);
setError(null);
setSuccess(null);
try {
const pool = pools.find(p => p.poolId === selectedPool);
if (!pool) throw new Error('Pool not found');
const amountBN = BigInt(Math.floor(parseFloat(unstakeAmount) * 1e12));
const injector = await web3FromAddress(selectedAccount.address);
const tx = assetHubApi.tx.assetRewards.unstake(selectedPool, amountBN.toString());
await new Promise<void>((resolve, reject) => {
tx.signAndSend(
selectedAccount.address,
{ signer: injector.signer },
({ status, dispatchError }) => {
if (status.isFinalized) {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`));
} else {
reject(new Error(dispatchError.toString()));
}
} else {
resolve();
}
}
}
);
});
setSuccess(`Successfully unstaked ${unstakeAmount} ${pool.stakedAsset}!`);
setUnstakeAmount('');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to unstake');
} finally {
setIsProcessing(false);
}
};
const handleHarvest = async () => {
if (!assetHubApi || !selectedAccount || selectedPool === null) return;
setIsProcessing(true);
setError(null);
setSuccess(null);
try {
const injector = await web3FromAddress(selectedAccount.address);
const tx = assetHubApi.tx.assetRewards.harvestRewards(selectedPool);
await new Promise<void>((resolve, reject) => {
tx.signAndSend(
selectedAccount.address,
{ signer: injector.signer },
({ status, dispatchError }) => {
if (status.isFinalized) {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = assetHubApi.registry.findMetaError(dispatchError.asModule);
reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`));
} else {
reject(new Error(dispatchError.toString()));
}
} else {
resolve();
}
}
}
);
});
setSuccess('Successfully harvested rewards!');
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to harvest rewards');
} finally {
setIsProcessing(false);
}
};
if (!isOpen) return null;
const currentPool = pools.find(p => p.poolId === selectedPool);
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-gray-900 rounded-lg max-w-lg w-full p-6 border border-gray-700 max-h-[90vh] overflow-y-auto">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold text-white">LP Staking</h2>
<button onClick={onClose} className="text-gray-400 hover:text-white">
<X className="w-6 h-6" />
</button>
</div>
{error && (
<Alert className="mb-4 bg-red-900/20 border-red-500">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert className="mb-4 bg-green-900/20 border-green-500">
<AlertDescription>{success}</AlertDescription>
</Alert>
)}
{isLoading ? (
<div className="text-center py-8">
<Loader2 className="w-8 h-8 animate-spin mx-auto text-cyan-400" />
<p className="text-gray-400 mt-2">Loading staking pools...</p>
</div>
) : pools.length === 0 ? (
<Alert className="bg-yellow-900/20 border-yellow-500">
<Info className="h-4 w-4" />
<AlertDescription>
No staking pools found. Admin needs to create them first.
</AlertDescription>
</Alert>
) : (
<>
{/* Pool Selection */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">
Select Pool
</label>
<select
value={selectedPool ?? ''}
onChange={(e) => setSelectedPool(parseInt(e.target.value))}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white"
>
{pools.map((pool) => (
<option key={pool.poolId} value={pool.poolId}>
{pool.stakedAsset} {pool.rewardAsset}
</option>
))}
</select>
</div>
{/* Pool Stats */}
{currentPool && (
<div className="bg-gray-800/50 rounded-lg p-4 mb-6 space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-400">Total Staked:</span>
<span className="text-white">{formatAmount(currentPool.totalStaked)} LP</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Your Staked:</span>
<span className="text-white">{formatAmount(currentPool.userStaked)} LP</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Your LP Balance:</span>
<span className="text-white">{formatAmount(currentPool.lpBalance)} LP</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-400">Reward Rate:</span>
<span className="text-cyan-400">{formatAmount(currentPool.rewardRatePerBlock)} PEZ/block</span>
</div>
</div>
)}
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="w-full mb-4">
<TabsTrigger value="stake" className="flex-1">
<Lock className="w-4 h-4 mr-2" />
Stake
</TabsTrigger>
<TabsTrigger value="unstake" className="flex-1">
<Unlock className="w-4 h-4 mr-2" />
Unstake
</TabsTrigger>
<TabsTrigger value="harvest" className="flex-1">
<Gift className="w-4 h-4 mr-2" />
Harvest
</TabsTrigger>
</TabsList>
<TabsContent value="stake">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Amount to Stake
</label>
<div className="relative">
<input
type="number"
value={stakeAmount}
onChange={(e) => setStakeAmount(e.target.value)}
placeholder="0.0"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white"
disabled={isProcessing}
/>
<button
onClick={() => currentPool && setStakeAmount(formatAmount(currentPool.lpBalance))}
className="absolute right-3 top-3 text-cyan-400 text-sm hover:text-cyan-300"
>
MAX
</button>
</div>
</div>
<Button
onClick={handleStake}
disabled={isProcessing || !stakeAmount}
className="w-full bg-gradient-to-r from-green-600 to-cyan-600 h-12"
>
{isProcessing ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Staking...
</>
) : (
<>
<Lock className="w-4 h-4 mr-2" />
Stake LP Tokens
</>
)}
</Button>
</div>
</TabsContent>
<TabsContent value="unstake">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Amount to Unstake
</label>
<div className="relative">
<input
type="number"
value={unstakeAmount}
onChange={(e) => setUnstakeAmount(e.target.value)}
placeholder="0.0"
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-white"
disabled={isProcessing}
/>
<button
onClick={() => currentPool && setUnstakeAmount(formatAmount(currentPool.userStaked))}
className="absolute right-3 top-3 text-cyan-400 text-sm hover:text-cyan-300"
>
MAX
</button>
</div>
</div>
<Button
onClick={handleUnstake}
disabled={isProcessing || !unstakeAmount}
className="w-full bg-gradient-to-r from-orange-600 to-red-600 h-12"
>
{isProcessing ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Unstaking...
</>
) : (
<>
<Unlock className="w-4 h-4 mr-2" />
Unstake LP Tokens
</>
)}
</Button>
</div>
</TabsContent>
<TabsContent value="harvest">
<div className="space-y-4">
<div className="bg-gray-800/50 rounded-lg p-4 text-center">
<p className="text-gray-400 mb-2">Pending Rewards</p>
<p className="text-3xl font-bold text-cyan-400">
{currentPool ? formatAmount(currentPool.pendingRewards) : '0'} PEZ
</p>
</div>
<Button
onClick={handleHarvest}
disabled={isProcessing}
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 h-12"
>
{isProcessing ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Harvesting...
</>
) : (
<>
<Gift className="w-4 h-4 mr-2" />
Harvest Rewards
</>
)}
</Button>
</div>
</TabsContent>
</Tabs>
</>
)}
</div>
</div>
);
};