diff --git a/package.json b/package.json index 2f990ec..e14c18b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pezkuwi-telegram-miniapp", - "version": "1.0.129", + "version": "1.0.130", "type": "module", "description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards", "author": "Pezkuwichain Team", diff --git a/src/components/wallet/HEZStakingModal.tsx b/src/components/wallet/HEZStakingModal.tsx new file mode 100644 index 0000000..4d1f45f --- /dev/null +++ b/src/components/wallet/HEZStakingModal.tsx @@ -0,0 +1,616 @@ +/** + * HEZ Staking Modal for Telegram Mini App + * Allows users to stake HEZ on Relay Chain for Trust Score + */ + +import { useState, useEffect, useCallback } from 'react'; +import { X, Lock, Unlock, Users, AlertCircle, Loader2, Shield, TrendingUp } from 'lucide-react'; +import { useWallet } from '@/contexts/WalletContext'; +import { useTelegram } from '@/hooks/useTelegram'; +import { cn } from '@/lib/utils'; + +interface StakingInfo { + stash: string; + totalBonded: bigint; + active: bigint; + unlocking: { value: bigint; era: number }[]; + nominations: string[]; + rewardDestination: string; +} + +interface ValidatorInfo { + address: string; + commission: number; + blocked: boolean; + identity?: string; +} + +interface HEZStakingModalProps { + isOpen: boolean; + onClose: () => void; +} + +const UNITS = 1_000_000_000_000; // 10^12 + +export function HEZStakingModal({ isOpen, onClose }: HEZStakingModalProps) { + const { api, keypair, address, balance } = useWallet(); + const { hapticImpact, hapticNotification, showAlert } = useTelegram(); + + const [stakingInfo, setStakingInfo] = useState(null); + const [validators, setValidators] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(null); + const [activeTab, setActiveTab] = useState<'status' | 'bond' | 'nominate' | 'unbond'>('status'); + const [bondAmount, setBondAmount] = useState(''); + const [unbondAmount, setUnbondAmount] = useState(''); + const [selectedValidators, setSelectedValidators] = useState([]); + + // Fetch staking info + const fetchStakingInfo = useCallback(async () => { + if (!api || !address) return; + + setIsLoading(true); + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stakingPallet = api.query.staking as any; + if (!stakingPallet) { + setError('Staking palleti bulunamadı'); + setIsLoading(false); + return; + } + + // Check if user has bonded + const ledger = await stakingPallet.ledger(address); + + if (ledger && !ledger.isEmpty && !ledger.isNone) { + const ledgerData = ledger.isSome ? ledger.unwrap().toJSON() : ledger.toJSON(); + + // Get nominations + const nominations = await stakingPallet.nominators(address); + const nominationsData = nominations?.toJSON() as { targets?: string[] } | null; + + // Get payee + const payee = await stakingPallet.payee(address); + const payeeStr = payee?.toString() || 'Staked'; + + setStakingInfo({ + stash: ledgerData.stash || address, + totalBonded: BigInt(ledgerData.total || 0), + active: BigInt(ledgerData.active || 0), + unlocking: (ledgerData.unlocking || []).map( + (u: { value: string | number; era: number }) => ({ + value: BigInt(u.value), + era: u.era, + }) + ), + nominations: nominationsData?.targets || [], + rewardDestination: payeeStr, + }); + } else { + setStakingInfo(null); + } + + // Fetch validators + const validatorEntries = await stakingPallet.validators.entries(); + const validatorList: ValidatorInfo[] = validatorEntries + .slice(0, 20) // Limit to 20 validators + .map( + ([key, value]: [ + { args: [{ toString: () => string }] }, + { toJSON: () => { commission?: number; blocked?: boolean } }, + ]) => { + const addr = key.args[0].toString(); + const prefs = value.toJSON(); + return { + address: addr, + commission: (prefs.commission || 0) / 10000000, // Convert from Perbill + blocked: prefs.blocked || false, + }; + } + ) + .filter((v: ValidatorInfo) => !v.blocked); + + setValidators(validatorList); + } catch (err) { + console.error('Error fetching staking info:', err); + setError('Staking bilgileri alınamadı'); + } finally { + setIsLoading(false); + } + }, [api, address]); + + useEffect(() => { + if (isOpen && api && address) { + fetchStakingInfo(); + } + }, [isOpen, api, address, fetchStakingInfo]); + + const formatHEZ = (amount: bigint): string => { + const hez = Number(amount) / UNITS; + return hez.toLocaleString(undefined, { maximumFractionDigits: 4 }); + }; + + const handleBond = async () => { + if (!api || !keypair || !bondAmount) return; + + setIsProcessing(true); + setError(null); + + try { + const amountBN = BigInt(Math.floor(parseFloat(bondAmount) * UNITS)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const stakingPallet = api.tx.staking as any; + + let tx; + if (stakingInfo) { + // Already bonded, add to existing stake + tx = stakingPallet.bondExtra(amountBN.toString()); + } else { + // First time bonding - set payee to Staked (compound rewards) + tx = stakingPallet.bond(amountBN.toString(), 'Staked'); + } + + await new Promise((resolve, reject) => { + tx.signAndSend( + keypair, + ({ + status, + dispatchError, + }: { + status: { isFinalized: boolean }; + dispatchError?: { isModule: boolean; asModule: unknown; toString: () => string }; + }) => { + if (status.isFinalized) { + if (dispatchError) { + let errorMsg = 'Bond neserketî'; + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + errorMsg = `${decoded.section}.${decoded.name}`; + } + reject(new Error(errorMsg)); + } else { + resolve(); + } + } + } + ).catch(reject); + }); + + hapticNotification('success'); + showAlert(`${bondAmount} HEZ stake kirin serketî!`); + setBondAmount(''); + fetchStakingInfo(); + setActiveTab('status'); + } catch (err) { + console.error('Bond error:', err); + setError(err instanceof Error ? err.message : 'Bond neserketî'); + hapticNotification('error'); + } finally { + setIsProcessing(false); + } + }; + + const handleNominate = async () => { + if (!api || !keypair || selectedValidators.length === 0) return; + + setIsProcessing(true); + setError(null); + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tx = (api.tx.staking as any).nominate(selectedValidators); + + await new Promise((resolve, reject) => { + tx.signAndSend( + keypair, + ({ + status, + dispatchError, + }: { + status: { isFinalized: boolean }; + dispatchError?: { isModule: boolean; asModule: unknown; toString: () => string }; + }) => { + if (status.isFinalized) { + if (dispatchError) { + let errorMsg = 'Nominate neserketî'; + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + errorMsg = `${decoded.section}.${decoded.name}`; + } + reject(new Error(errorMsg)); + } else { + resolve(); + } + } + } + ).catch(reject); + }); + + hapticNotification('success'); + showAlert(`${selectedValidators.length} validator nominate kirin serketî!`); + fetchStakingInfo(); + setActiveTab('status'); + } catch (err) { + console.error('Nominate error:', err); + setError(err instanceof Error ? err.message : 'Nominate neserketî'); + hapticNotification('error'); + } finally { + setIsProcessing(false); + } + }; + + const handleUnbond = async () => { + if (!api || !keypair || !unbondAmount) return; + + setIsProcessing(true); + setError(null); + + try { + const amountBN = BigInt(Math.floor(parseFloat(unbondAmount) * UNITS)); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const tx = (api.tx.staking as any).unbond(amountBN.toString()); + + await new Promise((resolve, reject) => { + tx.signAndSend( + keypair, + ({ + status, + dispatchError, + }: { + status: { isFinalized: boolean }; + dispatchError?: { isModule: boolean; asModule: unknown; toString: () => string }; + }) => { + if (status.isFinalized) { + if (dispatchError) { + let errorMsg = 'Unbond neserketî'; + if (dispatchError.isModule) { + const decoded = api.registry.findMetaError(dispatchError.asModule); + errorMsg = `${decoded.section}.${decoded.name}`; + } + reject(new Error(errorMsg)); + } else { + resolve(); + } + } + } + ).catch(reject); + }); + + hapticNotification('success'); + showAlert(`${unbondAmount} HEZ unbond kirin serketî! (28 roj li bendê)`); + setUnbondAmount(''); + fetchStakingInfo(); + setActiveTab('status'); + } catch (err) { + console.error('Unbond error:', err); + setError(err instanceof Error ? err.message : 'Unbond neserketî'); + hapticNotification('error'); + } finally { + setIsProcessing(false); + } + }; + + const toggleValidator = (addr: string) => { + hapticImpact('light'); + setSelectedValidators((prev) => + prev.includes(addr) ? prev.filter((v) => v !== addr) : [...prev, addr].slice(0, 16) + ); + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* Header */} +
+
+ +

HEZ Staking

+
+ +
+ + {/* Content */} +
+ {isLoading ? ( +
+ +
+ ) : ( + <> + {/* Tabs */} +
+ {[ + { id: 'status' as const, label: 'Durum', icon: TrendingUp }, + { id: 'bond' as const, label: 'Bond', icon: Lock }, + { id: 'nominate' as const, label: 'Nominate', icon: Users }, + { id: 'unbond' as const, label: 'Unbond', icon: Unlock }, + ].map(({ id, label, icon: Icon }) => ( + + ))} +
+ + {/* Error */} + {error && ( +
+ + {error} +
+ )} + + {/* Status Tab */} + {activeTab === 'status' && ( +
+ {stakingInfo ? ( + <> +
+
Aktîf Stake
+
+ {formatHEZ(stakingInfo.active)} HEZ +
+
+ +
+
+ Giştî Bonded + {formatHEZ(stakingInfo.totalBonded)} HEZ +
+
+ Nominations + {stakingInfo.nominations.length} validator +
+
+ Xelat Armanc + {stakingInfo.rewardDestination} +
+ {stakingInfo.unlocking.length > 0 && ( +
+ Unbonding + + {stakingInfo.unlocking.length} chunk + +
+ )} +
+ + {stakingInfo.nominations.length > 0 && ( +
+
+ Nominated Validators +
+
+ {stakingInfo.nominations.map((addr, i) => ( +
+ {addr.slice(0, 8)}...{addr.slice(-8)} +
+ ))} +
+
+ )} + +
+

+ 💡 HEZ stake kirin Trust Score zêde dike. Herî kêm 1 meh stake bikî ji bo + bonusê. +

+
+ + ) : ( +
+ +

Hîn Stake Nekiriye

+

+ HEZ stake bike ji bo Trust Score qezenckirin +

+ +
+ )} +
+ )} + + {/* Bond Tab */} + {activeTab === 'bond' && ( +
+
+
+ Bakiyê Te + {balance || '0'} HEZ +
+ {stakingInfo && ( +
+ Niha Staked + {formatHEZ(stakingInfo.active)} HEZ +
+ )} +
+ +
+ +
+ setBondAmount(e.target.value)} + placeholder="0.0000" + className="w-full bg-secondary border border-border rounded-xl p-4 pr-20 text-lg" + /> + +
+
+ +
+

+ ⚠️ Stake kirinê paşê 28 roj li bendê ye ji bo vekişandinê. +

+
+ + +
+ )} + + {/* Nominate Tab */} + {activeTab === 'nominate' && ( +
+ {!stakingInfo ? ( +
+ +

+ Pêşî HEZ bond bike, paşê nominate bike +

+
+ ) : ( + <> +
+ Validator hilbijêre (max 16): {selectedValidators.length}/16 +
+ +
+ {validators.map((v) => ( + + ))} +
+ + + + )} +
+ )} + + {/* Unbond Tab */} + {activeTab === 'unbond' && ( +
+ {!stakingInfo ? ( +
+ +

Tu hîn stake nekiriye

+
+ ) : ( + <> +
+
+ Aktîf Stake + + {formatHEZ(stakingInfo.active)} HEZ + +
+
+ +
+ +
+ setUnbondAmount(e.target.value)} + placeholder="0.0000" + className="w-full bg-secondary border border-border rounded-xl p-4 pr-20 text-lg" + /> + +
+
+ +
+

+ ⚠️ Unbond kirin 28 roj digire. Paşê dikare vekişîne. +

+
+ + + + )} +
+ )} + + )} +
+
+
+ ); +} diff --git a/src/components/wallet/WalletDashboard.tsx b/src/components/wallet/WalletDashboard.tsx index 1651c04..29676e2 100644 --- a/src/components/wallet/WalletDashboard.tsx +++ b/src/components/wallet/WalletDashboard.tsx @@ -25,6 +25,7 @@ import { TokensCard } from './TokensCard'; import { SwapModal } from './SwapModal'; import { PoolsModal } from './PoolsModal'; import { LPStakingModal } from './LPStakingModal'; +import { HEZStakingModal } from './HEZStakingModal'; import { useWallet } from '@/contexts/WalletContext'; import { useTelegram } from '@/hooks/useTelegram'; import { formatAddress } from '@/lib/wallet-service'; @@ -58,7 +59,9 @@ 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); + const [isLPStakingModalOpen, setIsLPStakingModalOpen] = useState(false); + const [isHEZStakingModalOpen, setIsHEZStakingModalOpen] = useState(false); + const [isStakingSelectorOpen, setIsStakingSelectorOpen] = useState(false); // Subscribe to PEZ balance (Asset ID: 1) - Uses Asset Hub API useEffect(() => { @@ -608,7 +611,7 @@ export function WalletDashboard({ onDisconnect }: Props) { + + + + + + + + )} ); } diff --git a/src/version.json b/src/version.json index 6d77e62..f121a8b 100644 --- a/src/version.json +++ b/src/version.json @@ -1,5 +1,5 @@ { - "version": "1.0.129", - "buildTime": "2026-02-06T23:26:35.940Z", - "buildNumber": 1770420395941 + "version": "1.0.130", + "buildTime": "2026-02-06T23:32:34.947Z", + "buildNumber": 1770420754948 }