From 122e38e30609b0c2b8b4f016e7cd0e98db275d0c Mon Sep 17 00:00:00 2001 From: Kurdistan Tech Ministry Date: Sat, 7 Feb 2026 01:20:16 +0300 Subject: [PATCH] feat: add staking and presale buttons to wallet quick actions - Change quick actions grid from 2x2 to 2x3 with smaller buttons - Add LP Staking modal with stake/unstake/claim rewards functionality - Add Presale button with coming soon message --- package.json | 2 +- src/components/wallet/LPStakingModal.tsx | 446 ++++++++++++++++++++++ src/components/wallet/WalletDashboard.tsx | 73 ++-- src/version.json | 6 +- 4 files changed, 500 insertions(+), 27 deletions(-) create mode 100644 src/components/wallet/LPStakingModal.tsx diff --git a/package.json b/package.json index e041c74..fa745c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pezkuwi-telegram-miniapp", - "version": "1.0.123", + "version": "1.0.124", "type": "module", "description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards", "author": "Pezkuwichain Team", diff --git a/src/components/wallet/LPStakingModal.tsx b/src/components/wallet/LPStakingModal.tsx new file mode 100644 index 0000000..b889bb2 --- /dev/null +++ b/src/components/wallet/LPStakingModal.tsx @@ -0,0 +1,446 @@ +/** + * LP Staking Modal for Telegram Mini App + * Allows users to stake LP tokens and earn PEZ rewards + */ + +import { useState, useEffect } from 'react'; +import { X, Lock, Unlock, Gift, AlertCircle, Loader2 } from 'lucide-react'; +import { useWallet } from '@/contexts/WalletContext'; +import { useTelegram } from '@/hooks/useTelegram'; +import { cn } from '@/lib/utils'; + +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 = { + 0: 'HEZ-PEZ LP', + 1: 'HEZ-USDT LP', + 2: 'HEZ-DOT LP', +}; + +export function LPStakingModal({ isOpen, onClose }: LPStakingModalProps) { + const { assetHubApi, keypair, address } = useWallet(); + const { hapticImpact, hapticNotification, showAlert } = useTelegram(); + + const [pools, setPools] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(null); + const [selectedPool, setSelectedPool] = useState(null); + const [stakeAmount, setStakeAmount] = useState(''); + const [unstakeAmount, setUnstakeAmount] = useState(''); + const [activeTab, setActiveTab] = useState<'stake' | 'unstake' | 'rewards'>('stake'); + + useEffect(() => { + if (!assetHubApi || !isOpen) return; + + const fetchPools = async () => { + setIsLoading(true); + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const poolEntries = await (assetHubApi.query.assetRewards as any)?.pools?.entries(); + + if (!poolEntries) { + setError('Staking palleti amade nîne'); + setIsLoading(false); + return; + } + + const stakingPools: StakingPool[] = []; + + for (const [key, value] of poolEntries) { + const poolId = parseInt(key.args[0].toString()); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const poolData = value.toJSON() as any; + + const lpTokenId = poolData.stakedAssetId?.interior?.x2?.[1]?.generalIndex ?? poolId; + + let userStaked = '0'; + let pendingRewards = '0'; + let lpBalance = '0'; + + if (address) { + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stakeInfo = await (assetHubApi.query.assetRewards as any).poolStakers([ + poolId, + address, + ]); + if (stakeInfo && stakeInfo.isSome) { + const stakeData = stakeInfo.unwrap().toJSON(); + userStaked = stakeData.amount || '0'; + } + + // Fetch LP balance + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const lpBal = await (assetHubApi.query.poolAssets as any).account(lpTokenId, address); + if (lpBal && lpBal.isSome) { + const lpData = lpBal.unwrap().toJSON(); + lpBalance = lpData.balance || '0'; + } + } catch { + // Ignore errors + } + } + + 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('Staking pools bar nekirin'); + } finally { + setIsLoading(false); + } + }; + + fetchPools(); + }, [assetHubApi, isOpen, address, 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 || !keypair || selectedPool === null || !stakeAmount) return; + + setIsProcessing(true); + setError(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)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tx = (assetHubApi.tx.assetRewards as any).stake(selectedPool, amountBN.toString()); + + const hash = await tx.signAndSend(keypair); + hapticNotification('success'); + showAlert(`Stake serket! Hash: ${hash.toString().slice(0, 16)}...`); + setStakeAmount(''); + + // Refresh pools + setTimeout(() => { + window.location.reload(); + }, 2000); + } catch (err) { + console.error('Stake error:', err); + setError(err instanceof Error ? err.message : 'Stake neserketî'); + hapticNotification('error'); + } finally { + setIsProcessing(false); + } + }; + + const handleUnstake = async () => { + if (!assetHubApi || !keypair || selectedPool === null || !unstakeAmount) return; + + setIsProcessing(true); + setError(null); + + try { + const amountBN = BigInt(Math.floor(parseFloat(unstakeAmount) * 1e12)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tx = (assetHubApi.tx.assetRewards as any).unstake(selectedPool, amountBN.toString()); + + const hash = await tx.signAndSend(keypair); + hapticNotification('success'); + showAlert(`Unstake serket! Hash: ${hash.toString().slice(0, 16)}...`); + setUnstakeAmount(''); + + setTimeout(() => { + window.location.reload(); + }, 2000); + } catch (err) { + console.error('Unstake error:', err); + setError(err instanceof Error ? err.message : 'Unstake neserketî'); + hapticNotification('error'); + } finally { + setIsProcessing(false); + } + }; + + const handleClaimRewards = async () => { + if (!assetHubApi || !keypair || selectedPool === null) return; + + setIsProcessing(true); + setError(null); + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tx = (assetHubApi.tx.assetRewards as any).harvestRewards(selectedPool); + + const hash = await tx.signAndSend(keypair); + hapticNotification('success'); + showAlert(`Xelat hat stendin! Hash: ${hash.toString().slice(0, 16)}...`); + + setTimeout(() => { + window.location.reload(); + }, 2000); + } catch (err) { + console.error('Claim error:', err); + setError(err instanceof Error ? err.message : 'Xelat stendin neserketî'); + hapticNotification('error'); + } finally { + setIsProcessing(false); + } + }; + + const currentPool = pools.find((p) => p.poolId === selectedPool); + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+

LP Staking

+ +
+ + {/* Content */} +
+ {isLoading ? ( +
+ +
+ ) : pools.length === 0 ? ( +
+ +

Hêj staking pool tune ne

+
+ ) : ( + <> + {/* Pool Selector */} +
+ +
+ {pools.map((pool) => ( + + ))} +
+
+ + {/* Pool Stats */} + {currentPool && ( +
+
+
+
Giştî Staked
+
{formatAmount(currentPool.totalStaked)}
+
+
+
Te Stake Kiriye
+
+ {formatAmount(currentPool.userStaked)} +
+
+
+
LP Bakiye
+
{formatAmount(currentPool.lpBalance)}
+
+
+
Xelat
+
+ {formatAmount(currentPool.pendingRewards)} PEZ +
+
+
+
+ )} + + {/* Tabs */} +
+ {[ + { id: 'stake' as const, label: 'Stake', icon: Lock }, + { id: 'unstake' as const, label: 'Unstake', icon: Unlock }, + { id: 'rewards' as const, label: 'Xelat', icon: Gift }, + ].map(({ id, label, icon: Icon }) => ( + + ))} +
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Stake Tab */} + {activeTab === 'stake' && currentPool && ( +
+
+ +
+ setStakeAmount(e.target.value)} + placeholder="0.0000" + className="w-full bg-secondary border border-border rounded-xl p-4 pr-20 text-lg" + /> + +
+
+ Bakiye: {formatAmount(currentPool.lpBalance)} +
+
+ +
+ )} + + {/* Unstake Tab */} + {activeTab === 'unstake' && currentPool && ( +
+
+ +
+ setUnstakeAmount(e.target.value)} + placeholder="0.0000" + className="w-full bg-secondary border border-border rounded-xl p-4 pr-20 text-lg" + /> + +
+
+ Staked: {formatAmount(currentPool.userStaked)} +
+
+ +
+ )} + + {/* Rewards Tab */} + {activeTab === 'rewards' && currentPool && ( +
+
+ +
+ {formatAmount(currentPool.pendingRewards)} PEZ +
+
Xelatên li bendê
+
+ +
+ )} + + )} +
+
+
+ ); +} diff --git a/src/components/wallet/WalletDashboard.tsx b/src/components/wallet/WalletDashboard.tsx index aff417c..1651c04 100644 --- a/src/components/wallet/WalletDashboard.tsx +++ b/src/components/wallet/WalletDashboard.tsx @@ -17,11 +17,14 @@ import { ScanLine, ArrowLeftRight, Droplets, + Coins, + Rocket, } from 'lucide-react'; import QRCode from 'qrcode'; import { TokensCard } from './TokensCard'; import { SwapModal } from './SwapModal'; import { PoolsModal } from './PoolsModal'; +import { LPStakingModal } from './LPStakingModal'; import { useWallet } from '@/contexts/WalletContext'; import { useTelegram } from '@/hooks/useTelegram'; import { formatAddress } from '@/lib/wallet-service'; @@ -45,7 +48,7 @@ interface Transaction { export function WalletDashboard({ onDisconnect }: Props) { const { address, balance, api, assetHubApi, disconnect, isLoading } = useWallet(); - const { hapticImpact, hapticNotification } = useTelegram(); + const { hapticImpact, hapticNotification, showAlert } = useTelegram(); const [copied, setCopied] = useState(false); const [activeTab, setActiveTab] = useState<'main' | 'send' | 'receive' | 'history'>('main'); @@ -55,6 +58,7 @@ export function WalletDashboard({ onDisconnect }: Props) { const [isLoadingTxs, setIsLoadingTxs] = useState(false); const [isSwapModalOpen, setIsSwapModalOpen] = useState(false); const [isPoolsModalOpen, setIsPoolsModalOpen] = useState(false); + const [isStakingModalOpen, setIsStakingModalOpen] = useState(false); // Subscribe to PEZ balance (Asset ID: 1) - Uses Asset Hub API useEffect(() => { @@ -547,20 +551,19 @@ export function WalletDashboard({ onDisconnect }: Props) { - {/* Quick Actions - 2x2 Grid */} -
+ {/* Quick Actions - 2x3 Grid */} +
+ + + +
@@ -698,6 +724,7 @@ export function WalletDashboard({ onDisconnect }: Props) { {/* Modals */} setIsSwapModalOpen(false)} /> setIsPoolsModalOpen(false)} /> + setIsStakingModalOpen(false)} />
); } diff --git a/src/version.json b/src/version.json index 098545a..d5a428b 100644 --- a/src/version.json +++ b/src/version.json @@ -1,5 +1,5 @@ { - "version": "1.0.123", - "buildTime": "2026-02-06T22:10:09.600Z", - "buildNumber": 1770415809601 + "version": "1.0.124", + "buildTime": "2026-02-06T22:20:16.985Z", + "buildNumber": 1770416416986 }