mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 04:27:56 +00:00
feat: add LP staking modal and reward pools creation script
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Script to create LP staking reward pools
|
||||
* Run with: node scripts/create-staking-pools.mjs
|
||||
*
|
||||
* Requires MNEMONIC environment variable with admin wallet seed
|
||||
*/
|
||||
|
||||
import { ApiPromise, WsProvider } from '@pezkuwi/api';
|
||||
import { Keyring } from '@pezkuwi/api';
|
||||
|
||||
const ASSET_HUB_RPC = 'wss://asset-hub-rpc.pezkuwichain.io';
|
||||
|
||||
// LP Token IDs (from assetConversion pools)
|
||||
const LP_TOKENS = {
|
||||
'HEZ-PEZ': 0,
|
||||
'HEZ-USDT': 1,
|
||||
'HEZ-DOT': 2,
|
||||
};
|
||||
|
||||
// Reward token: PEZ (asset ID 1)
|
||||
const REWARD_ASSET_ID = 1;
|
||||
|
||||
// Reward rate per block (in smallest units - 12 decimals)
|
||||
// 0.01 PEZ per block = 10_000_000_000 (10^10)
|
||||
const REWARD_RATE_PER_BLOCK = '10000000000';
|
||||
|
||||
// 100 years in blocks (6 second blocks)
|
||||
const BLOCKS_100_YEARS = 525600000;
|
||||
|
||||
async function main() {
|
||||
const mnemonic = process.env.MNEMONIC;
|
||||
if (!mnemonic) {
|
||||
console.error('ERROR: Set MNEMONIC environment variable');
|
||||
console.log('Usage: MNEMONIC="foam hope topic phone year fold lyrics biology erosion feed false island" node scripts/create-staking-pools.mjs');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const provider = new WsProvider(ASSET_HUB_RPC);
|
||||
const api = await ApiPromise.create({ provider });
|
||||
|
||||
const keyring = new Keyring({ type: 'sr25519' });
|
||||
const admin = keyring.addFromMnemonic(mnemonic);
|
||||
console.log('Admin address:', admin.address);
|
||||
|
||||
// Get current block for expiry calculation
|
||||
const header = await api.rpc.chain.getHeader();
|
||||
const currentBlock = header.number.toNumber();
|
||||
const expiryBlock = currentBlock + BLOCKS_100_YEARS;
|
||||
console.log('Current block:', currentBlock);
|
||||
console.log('Expiry block (100 years):', expiryBlock);
|
||||
|
||||
// Format asset location for LP tokens (poolAssets pallet, instance 55)
|
||||
const formatLpTokenLocation = (lpTokenId) => ({
|
||||
parents: 0,
|
||||
interior: { X2: [{ PalletInstance: 55 }, { GeneralIndex: lpTokenId }] }
|
||||
});
|
||||
|
||||
// Format asset location for reward token (assets pallet, instance 50)
|
||||
const formatRewardTokenLocation = (assetId) => ({
|
||||
parents: 0,
|
||||
interior: { X2: [{ PalletInstance: 50 }, { GeneralIndex: assetId }] }
|
||||
});
|
||||
|
||||
// Expiry format: { At: blockNumber }
|
||||
const expiry = { At: expiryBlock };
|
||||
|
||||
console.log('\n=== Creating Staking Pools ===\n');
|
||||
|
||||
for (const [poolName, lpTokenId] of Object.entries(LP_TOKENS)) {
|
||||
console.log(`Creating pool for ${poolName} (LP Token #${lpTokenId})...`);
|
||||
|
||||
const stakedAssetLocation = formatLpTokenLocation(lpTokenId);
|
||||
const rewardAssetLocation = formatRewardTokenLocation(REWARD_ASSET_ID);
|
||||
|
||||
try {
|
||||
const tx = api.tx.assetRewards.createPool(
|
||||
stakedAssetLocation,
|
||||
rewardAssetLocation,
|
||||
REWARD_RATE_PER_BLOCK,
|
||||
expiry,
|
||||
admin.address // admin
|
||||
);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
tx.signAndSend(admin, ({ status, events, dispatchError }) => {
|
||||
if (status.isInBlock) {
|
||||
console.log(` In block: ${status.asInBlock.toHex()}`);
|
||||
} else if (status.isFinalized) {
|
||||
console.log(` Finalized: ${status.asFinalized.toHex()}`);
|
||||
|
||||
if (dispatchError) {
|
||||
if (dispatchError.isModule) {
|
||||
const decoded = api.registry.findMetaError(dispatchError.asModule);
|
||||
reject(new Error(`${decoded.section}.${decoded.name}: ${decoded.docs.join(' ')}`));
|
||||
} else {
|
||||
reject(new Error(dispatchError.toString()));
|
||||
}
|
||||
} else {
|
||||
// Find pool created event
|
||||
const poolCreated = events.find(({ event }) =>
|
||||
event.section === 'assetRewards' && event.method === 'PoolCreated'
|
||||
);
|
||||
if (poolCreated) {
|
||||
console.log(` ✅ Pool created:`, poolCreated.event.data.toHuman());
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log(` ✅ ${poolName} staking pool created!\n`);
|
||||
} catch (err) {
|
||||
console.error(` ❌ Failed to create ${poolName} pool:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// List created pools
|
||||
console.log('\n=== Created Pools ===');
|
||||
const pools = await api.query.assetRewards.pools.entries();
|
||||
for (const [key, value] of pools) {
|
||||
console.log('Pool ID:', key.args[0].toString());
|
||||
console.log(' Config:', value.toHuman());
|
||||
}
|
||||
|
||||
await api.disconnect();
|
||||
console.log('\nDone!');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,474 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { TrendingUp, Droplet, DollarSign, Percent, Info, AlertTriangle, BarChart3, Clock } from 'lucide-react';
|
||||
import { TrendingUp, Droplet, DollarSign, Percent, Info, AlertTriangle, BarChart3, Clock, Lock } from 'lucide-react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||
@@ -11,6 +11,7 @@ import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||
import { NATIVE_TOKEN_ID } from '@/types/dex';
|
||||
import { AddLiquidityModal } from '@/components/AddLiquidityModal';
|
||||
import { RemoveLiquidityModal } from '@/components/RemoveLiquidityModal';
|
||||
import { LPStakingModal } from '@/components/LPStakingModal';
|
||||
|
||||
// Helper function to convert asset IDs to user-friendly display names
|
||||
// Users should only see HEZ, PEZ, USDT - wrapped tokens are backend details
|
||||
@@ -51,6 +52,7 @@ const PoolDashboard = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isAddLiquidityModalOpen, setIsAddLiquidityModalOpen] = useState(false);
|
||||
const [isRemoveLiquidityModalOpen, setIsRemoveLiquidityModalOpen] = useState(false);
|
||||
const [isStakingModalOpen, setIsStakingModalOpen] = useState(false);
|
||||
|
||||
// Pool selection state
|
||||
const [availablePools, setAvailablePools] = useState<Array<[number, number]>>([]);
|
||||
@@ -577,13 +579,20 @@ const PoolDashboard = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3 pt-2">
|
||||
<div className="grid grid-cols-3 gap-3 pt-2">
|
||||
<Button
|
||||
onClick={() => setIsAddLiquidityModalOpen(true)}
|
||||
className="bg-gradient-to-r from-green-600 to-blue-600 hover:from-green-700 hover:to-blue-700"
|
||||
>
|
||||
Add More
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsStakingModalOpen(true)}
|
||||
className="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700"
|
||||
>
|
||||
<Lock className="w-4 h-4 mr-1" />
|
||||
Stake LP
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsRemoveLiquidityModalOpen(true)}
|
||||
variant="outline"
|
||||
@@ -658,6 +667,11 @@ const PoolDashboard = () => {
|
||||
asset1={poolData.asset1}
|
||||
/>
|
||||
)}
|
||||
|
||||
<LPStakingModal
|
||||
isOpen={isStakingModalOpen}
|
||||
onClose={() => setIsStakingModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user