diff --git a/web/src/components/AccountBalance.tsx b/web/src/components/AccountBalance.tsx index a1bbe2f3..d574631b 100644 --- a/web/src/components/AccountBalance.tsx +++ b/web/src/components/AccountBalance.tsx @@ -1,12 +1,13 @@ import React, { useEffect, useState } from 'react'; import { usePezkuwi } from '@/contexts/PezkuwiContext'; 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 { ASSET_IDS, getAssetSymbol } from '@pezkuwi/lib/wallet'; import { AddTokenModal } from './AddTokenModal'; import { TransferModal } from './TransferModal'; import { XCMTeleportModal } from './XCMTeleportModal'; +import { LPStakeModal } from './LPStakeModal'; import { getAllScores, type UserScores } from '@pezkuwi/lib/scores'; interface TokenBalance { @@ -57,6 +58,8 @@ export const AccountBalance: React.FC = () => { const stored = localStorage.getItem('customTokenIds'); return stored ? JSON.parse(stored) : []; }); + const [isLPStakeModalOpen, setIsLPStakeModalOpen] = useState(false); + const [selectedLPForStake, setSelectedLPForStake] = useState(null); // Helper function to get asset decimals const getAssetDecimals = (assetId: number): number => { @@ -89,6 +92,7 @@ export const AccountBalance: React.FC = () => { ETH: '/tokens/ETH.png', 'HEZ-PEZ LP': '/tokens/LP.png', 'HEZ-USDT LP': '/tokens/LP.png', + 'HEZ-DOT LP': '/tokens/LP.png', }; // 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); if (import.meta.env.DEV) console.log('✅ LP tokens fetched:', lpTokensData); } @@ -884,7 +906,7 @@ export const AccountBalance: React.FC = () => {
{lpTokens.map((lp) => ( -
+
{lp.symbol}
@@ -892,9 +914,23 @@ export const AccountBalance: React.FC = () => {
{lp.name}
-
-
{lp.balance}
-
Pool Share
+
+
+
{lp.balance}
+
Pool Share
+
+
))} @@ -1108,6 +1144,17 @@ export const AccountBalance: React.FC = () => { isOpen={isXCMTeleportModalOpen} onClose={() => setIsXCMTeleportModalOpen(false)} /> + + {/* LP Stake Modal */} + { + setIsLPStakeModalOpen(false); + setSelectedLPForStake(null); + }} + lpToken={selectedLPForStake} + onStakeSuccess={() => fetchBalance()} + />
); }; diff --git a/web/src/components/LPStakeModal.tsx b/web/src/components/LPStakeModal.tsx new file mode 100644 index 00000000..0b2af4f4 --- /dev/null +++ b/web/src/components/LPStakeModal.tsx @@ -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 = { + 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 = ({ + isOpen, + onClose, + lpToken, + onStakeSuccess, +}) => { + const { assetHubApi, selectedAccount, isAssetHubReady } = usePezkuwi(); + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const [stakeAmount, setStakeAmount] = useState(''); + const [selectedDuration, setSelectedDuration] = useState(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((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 ( +
+
+
+
+
+ +
+
+

LP Token Stake

+

{lpToken.symbol}

+
+
+ +
+ + {error && ( + + + {error} + + )} + + {success && ( + + {success} + + )} + +
+ {/* Duration Selection */} +
+ +
+ {DURATION_OPTIONS.map((option) => ( + + ))} +
+
+ + {/* Balance Info */} +
+
+ Mevcut Bakiye: + {lpToken.balance} {lpToken.symbol} +
+ {selectedDurationOption && ( +
+ Ödül Çarpanı: + {selectedDurationOption.multiplier}x +
+ )} +
+ + {/* Amount Input */} +
+ +
+ 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" + /> + +
+
+ Pool ID: {poolId} +
+
+ + {/* Warning */} +
+
+ +
+ 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. +
+
+
+ + {/* Stake Button */} + +
+
+
+ ); +};