mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-13 22:41:02 +00:00
feat: add LP token staking with duration selection
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Wallet, TrendingUp, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel } from 'lucide-react';
|
import { Wallet, TrendingUp, RefreshCw, Award, Plus, Coins, Send, Shield, Users, Fuel, Lock } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
import { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet';
|
||||||
import { AddTokenModal } from './AddTokenModal';
|
import { AddTokenModal } from './AddTokenModal';
|
||||||
import { TransferModal } from './TransferModal';
|
import { TransferModal } from './TransferModal';
|
||||||
import { XCMTeleportModal } from './XCMTeleportModal';
|
import { XCMTeleportModal } from './XCMTeleportModal';
|
||||||
|
import { LPStakeModal } from './LPStakeModal';
|
||||||
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
import { getAllScores, type UserScores } from '@pezkuwi/lib/scores';
|
||||||
|
|
||||||
interface TokenBalance {
|
interface TokenBalance {
|
||||||
@@ -57,6 +58,8 @@ export const AccountBalance: React.FC = () => {
|
|||||||
const stored = localStorage.getItem('customTokenIds');
|
const stored = localStorage.getItem('customTokenIds');
|
||||||
return stored ? JSON.parse(stored) : [];
|
return stored ? JSON.parse(stored) : [];
|
||||||
});
|
});
|
||||||
|
const [isLPStakeModalOpen, setIsLPStakeModalOpen] = useState(false);
|
||||||
|
const [selectedLPForStake, setSelectedLPForStake] = useState<TokenBalance | null>(null);
|
||||||
|
|
||||||
// Helper function to get asset decimals
|
// Helper function to get asset decimals
|
||||||
const getAssetDecimals = (assetId: number): number => {
|
const getAssetDecimals = (assetId: number): number => {
|
||||||
@@ -89,6 +92,7 @@ export const AccountBalance: React.FC = () => {
|
|||||||
ETH: '/tokens/ETH.png',
|
ETH: '/tokens/ETH.png',
|
||||||
'HEZ-PEZ LP': '/tokens/LP.png',
|
'HEZ-PEZ LP': '/tokens/LP.png',
|
||||||
'HEZ-USDT LP': '/tokens/LP.png',
|
'HEZ-USDT LP': '/tokens/LP.png',
|
||||||
|
'HEZ-DOT LP': '/tokens/LP.png',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get token logo URL
|
// Get token logo URL
|
||||||
@@ -481,6 +485,24 @@ export const AccountBalance: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HEZ-DOT LP Token (ID: 2)
|
||||||
|
const hezDotLp = await assetHubApi.query.poolAssets.account(2, selectedAccount.address);
|
||||||
|
if (hezDotLp.isSome) {
|
||||||
|
const lpBalance = hezDotLp.unwrap().balance.toString();
|
||||||
|
const lpTokens = (parseInt(lpBalance) / divisor).toFixed(4);
|
||||||
|
if (parseFloat(lpTokens) > 0) {
|
||||||
|
lpTokensData.push({
|
||||||
|
assetId: 2,
|
||||||
|
symbol: 'HEZ-DOT LP',
|
||||||
|
name: 'HEZ-DOT Liquidity',
|
||||||
|
balance: lpTokens,
|
||||||
|
decimals: 12,
|
||||||
|
usdValue: 0, // TODO: Calculate LP value
|
||||||
|
isLpToken: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setLpTokens(lpTokensData);
|
setLpTokens(lpTokensData);
|
||||||
if (import.meta.env.DEV) console.log('✅ LP tokens fetched:', lpTokensData);
|
if (import.meta.env.DEV) console.log('✅ LP tokens fetched:', lpTokensData);
|
||||||
}
|
}
|
||||||
@@ -884,7 +906,7 @@ export const AccountBalance: React.FC = () => {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{lpTokens.map((lp) => (
|
{lpTokens.map((lp) => (
|
||||||
<div key={lp.assetId} className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg">
|
<div key={lp.assetId} className="flex items-center justify-between p-3 bg-gray-800/50 rounded-lg group">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<img src="/tokens/LP.png" alt={lp.symbol} className="w-8 h-8 rounded-full" />
|
<img src="/tokens/LP.png" alt={lp.symbol} className="w-8 h-8 rounded-full" />
|
||||||
<div>
|
<div>
|
||||||
@@ -892,9 +914,23 @@ export const AccountBalance: React.FC = () => {
|
|||||||
<div className="text-xs text-gray-400">{lp.name}</div>
|
<div className="text-xs text-gray-400">{lp.name}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-lg font-semibold text-white">{lp.balance}</div>
|
<div className="text-right">
|
||||||
<div className="text-xs text-gray-500">Pool Share</div>
|
<div className="text-lg font-semibold text-white">{lp.balance}</div>
|
||||||
|
<div className="text-xs text-gray-500">Pool Share</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedLPForStake(lp);
|
||||||
|
setIsLPStakeModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="border-purple-500/50 text-purple-400 hover:bg-purple-500/10 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
<Lock className="w-3 h-3 mr-1" />
|
||||||
|
Stake
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -1108,6 +1144,17 @@ export const AccountBalance: React.FC = () => {
|
|||||||
isOpen={isXCMTeleportModalOpen}
|
isOpen={isXCMTeleportModalOpen}
|
||||||
onClose={() => setIsXCMTeleportModalOpen(false)}
|
onClose={() => setIsXCMTeleportModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* LP Stake Modal */}
|
||||||
|
<LPStakeModal
|
||||||
|
isOpen={isLPStakeModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIsLPStakeModalOpen(false);
|
||||||
|
setSelectedLPForStake(null);
|
||||||
|
}}
|
||||||
|
lpToken={selectedLPForStake}
|
||||||
|
onStakeSuccess={() => fetchBalance()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { X, Lock, AlertCircle, Loader2, Clock } 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';
|
||||||
|
|
||||||
|
interface TokenBalance {
|
||||||
|
assetId: number;
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
balance: string;
|
||||||
|
decimals: number;
|
||||||
|
usdValue: number;
|
||||||
|
isLpToken?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LPStakeModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
lpToken: TokenBalance | null;
|
||||||
|
onStakeSuccess?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool ID mapping: LP Token assetId -> Staking Pool ID
|
||||||
|
const LP_TO_POOL_ID: Record<number, number> = {
|
||||||
|
0: 0, // HEZ-PEZ LP -> Pool 0
|
||||||
|
1: 1, // HEZ-USDT LP -> Pool 1
|
||||||
|
2: 2, // HEZ-DOT LP -> Pool 2
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DurationOption {
|
||||||
|
label: string;
|
||||||
|
months: number;
|
||||||
|
multiplier: number; // Reward multiplier (for display)
|
||||||
|
}
|
||||||
|
|
||||||
|
const DURATION_OPTIONS: DurationOption[] = [
|
||||||
|
{ label: '1 Ay', months: 1, multiplier: 1 },
|
||||||
|
{ label: '3 Ay', months: 3, multiplier: 1.5 },
|
||||||
|
{ label: '6 Ay', months: 6, multiplier: 2 },
|
||||||
|
{ label: '1 Yıl', months: 12, multiplier: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LPStakeModal: React.FC<LPStakeModalProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
lpToken,
|
||||||
|
onStakeSuccess,
|
||||||
|
}) => {
|
||||||
|
const { assetHubApi, selectedAccount, isAssetHubReady } = usePezkuwi();
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
const [stakeAmount, setStakeAmount] = useState('');
|
||||||
|
const [selectedDuration, setSelectedDuration] = useState<number>(1); // months
|
||||||
|
|
||||||
|
if (!isOpen || !lpToken) return null;
|
||||||
|
|
||||||
|
const poolId = LP_TO_POOL_ID[lpToken.assetId];
|
||||||
|
const maxBalance = parseFloat(lpToken.balance);
|
||||||
|
|
||||||
|
const handleStake = async () => {
|
||||||
|
if (!assetHubApi || !isAssetHubReady || !selectedAccount || poolId === undefined) {
|
||||||
|
setError('API bağlantısı hazır değil');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = parseFloat(stakeAmount);
|
||||||
|
if (isNaN(amount) || amount <= 0) {
|
||||||
|
setError('Geçerli bir miktar girin');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount > maxBalance) {
|
||||||
|
setError('Yetersiz LP token bakiyesi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const amountBN = BigInt(Math.floor(amount * 1e12));
|
||||||
|
const injector = await web3FromAddress(selectedAccount.address);
|
||||||
|
|
||||||
|
const tx = assetHubApi.tx.assetRewards.stake(poolId, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const durationLabel = DURATION_OPTIONS.find(d => d.months === selectedDuration)?.label || `${selectedDuration} ay`;
|
||||||
|
setSuccess(`${stakeAmount} ${lpToken.symbol} başarıyla ${durationLabel} süreyle stake edildi!`);
|
||||||
|
setStakeAmount('');
|
||||||
|
|
||||||
|
if (onStakeSuccess) {
|
||||||
|
onStakeSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal after success
|
||||||
|
setTimeout(() => {
|
||||||
|
onClose();
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Stake işlemi başarısız oldu');
|
||||||
|
} finally {
|
||||||
|
setIsProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMaxAmount = () => {
|
||||||
|
setStakeAmount(lpToken.balance);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedDurationOption = DURATION_OPTIONS.find(d => d.months === selectedDuration);
|
||||||
|
|
||||||
|
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-md w-full p-6 border border-gray-700">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-purple-500/20 rounded-lg">
|
||||||
|
<Lock className="w-5 h-5 text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-white">LP Token Stake</h2>
|
||||||
|
<p className="text-sm text-gray-400">{lpToken.symbol}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-5">
|
||||||
|
{/* Duration Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-3">
|
||||||
|
<Clock className="w-4 h-4 inline mr-2" />
|
||||||
|
Stake Süresi
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{DURATION_OPTIONS.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.months}
|
||||||
|
onClick={() => setSelectedDuration(option.months)}
|
||||||
|
className={`p-3 rounded-lg border text-center transition-all ${
|
||||||
|
selectedDuration === option.months
|
||||||
|
? 'border-purple-500 bg-purple-500/20 text-purple-300'
|
||||||
|
: 'border-gray-700 bg-gray-800/50 text-gray-400 hover:border-gray-600'
|
||||||
|
}`}
|
||||||
|
disabled={isProcessing}
|
||||||
|
>
|
||||||
|
<div className="text-sm font-medium">{option.label}</div>
|
||||||
|
<div className="text-xs mt-1 text-gray-500">{option.multiplier}x</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Balance Info */}
|
||||||
|
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-400">Mevcut Bakiye:</span>
|
||||||
|
<span className="text-white font-medium">{lpToken.balance} {lpToken.symbol}</span>
|
||||||
|
</div>
|
||||||
|
{selectedDurationOption && (
|
||||||
|
<div className="flex justify-between text-sm mt-2">
|
||||||
|
<span className="text-gray-400">Ödül Çarpanı:</span>
|
||||||
|
<span className="text-purple-400 font-medium">{selectedDurationOption.multiplier}x</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Amount Input */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Stake Miktarı
|
||||||
|
</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 pr-20"
|
||||||
|
disabled={isProcessing}
|
||||||
|
max={maxBalance}
|
||||||
|
min={0}
|
||||||
|
step="0.0001"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={setMaxAmount}
|
||||||
|
className="absolute right-3 top-3 text-purple-400 text-sm hover:text-purple-300 font-medium"
|
||||||
|
disabled={isProcessing}
|
||||||
|
>
|
||||||
|
MAX
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
Pool ID: {poolId}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Warning */}
|
||||||
|
<div className="bg-yellow-900/20 border border-yellow-500/30 rounded-lg p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<AlertCircle className="w-4 h-4 text-yellow-400 mt-0.5 flex-shrink-0" />
|
||||||
|
<div className="text-xs text-yellow-300">
|
||||||
|
LP tokenlarınız seçilen süre boyunca kilitlenecektir. Bu süre içinde unstake yapamazsınız.
|
||||||
|
Ödüller her blokta otomatik olarak birikir.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stake Button */}
|
||||||
|
<Button
|
||||||
|
onClick={handleStake}
|
||||||
|
disabled={isProcessing || !stakeAmount || parseFloat(stakeAmount) <= 0}
|
||||||
|
className="w-full bg-gradient-to-r from-purple-600 to-pink-600 h-12 text-base font-medium"
|
||||||
|
>
|
||||||
|
{isProcessing ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Stake Ediliyor...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Lock className="w-4 h-4 mr-2" />
|
||||||
|
{selectedDurationOption?.label} Stake Et
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user