feat: add PEZ presale system with wUSDT contribution

Implemented complete presale system for PEZ token distribution:

Backend (Pallet):
- Created pallet-presale at /home/mamostehp/Pezkuwi-SDK/pezkuwi/pallets/presale/
- Accepts wUSDT (Asset ID 2) contributions
- Tracks all contributors and amounts
- Distributes PEZ (Asset ID 1) after 45-day period
- Conversion rate: 1 wUSDT = 100 PEZ
- Includes emergency pause/unpause functionality
- Runtime integration documentation provided

Frontend:
- Created Presale page with contribution form
- Live stats: time remaining, total raised, contributors count
- Real-time balance display and conversion calculator
- Progress bar showing fundraising goal ($1M target)
- Added route /presale and navigation under Trading menu
- Connected to PolkadotContext and WalletContext

Technical Details:
- wUSDT: 6 decimals (Asset ID 2)
- PEZ: 12 decimals (Asset ID 1)
- Duration: 648,000 blocks (45 days @ 6s blocks)
- Treasury: PalletId "py/prsal"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 07:05:48 +03:00
parent db8cb44db0
commit 3b377ea857
3 changed files with 382 additions and 1 deletions
+2
View File
@@ -37,6 +37,7 @@ const Elections = lazy(() => import('./pages/Elections'));
const EducationPlatform = lazy(() => import('./pages/EducationPlatform'));
const P2PPlatform = lazy(() => import('./pages/P2PPlatform'));
const DEXDashboard = lazy(() => import('./components/dex/DEXDashboard').then(m => ({ default: m.DEXDashboard })));
const Presale = lazy(() => import('./pages/Presale'));
const NotFound = lazy(() => import('@/pages/NotFound'));
// Loading component
@@ -137,6 +138,7 @@ function App() {
<DEXDashboard />
</ProtectedRoute>
} />
<Route path="/presale" element={<Presale />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
+8 -1
View File
@@ -19,7 +19,7 @@ import { TreasuryOverview } from './treasury/TreasuryOverview';
import { FundingProposal } from './treasury/FundingProposal';
import { SpendingHistory } from './treasury/SpendingHistory';
import { MultiSigApproval } from './treasury/MultiSigApproval';
import { ExternalLink, Award, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, Users, Droplet, Mail } from 'lucide-react';
import { ExternalLink, Award, FileEdit, Users2, MessageSquare, ShieldCheck, Wifi, WifiOff, Wallet, DollarSign, PiggyBank, History, Key, TrendingUp, ArrowRightLeft, Lock, LogIn, LayoutDashboard, Settings, Users, Droplet, Mail, Coins } from 'lucide-react';
import GovernanceInterface from './GovernanceInterface';
import RewardDistribution from './RewardDistribution';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@@ -192,6 +192,13 @@ const AppLayout: React.FC = () => {
<Users className="w-4 h-4" />
P2P
</button>
<button
onClick={() => navigate('/presale')}
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
>
<Coins className="w-4 h-4" />
Presale
</button>
<button
onClick={() => setShowStaking(true)}
className="w-full text-left px-4 py-2 text-gray-300 hover:bg-gray-800 hover:text-white flex items-center gap-2"
+372
View File
@@ -0,0 +1,372 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { useWallet } from '@/contexts/WalletContext';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Loader2, AlertCircle, CheckCircle2, Timer, TrendingUp, Users } from 'lucide-react';
import { toast } from 'sonner';
export default function Presale() {
const { t } = useTranslation();
const { api, selectedAccount, isApiReady } = usePolkadot();
const { balances } = useWallet();
const [wusdtAmount, setWusdtAmount] = useState('');
const [timeRemaining, setTimeRemaining] = useState(0);
const [totalRaised, setTotalRaised] = useState('0');
const [myContribution, setMyContribution] = useState('0');
const [active, setActive] = useState(false);
const [paused, setPaused] = useState(false);
const [loading, setLoading] = useState(false);
const [contributorsCount, setContributorsCount] = useState(0);
useEffect(() => {
if (isApiReady) {
loadPresaleData();
const interval = setInterval(loadPresaleData, 10000);
return () => clearInterval(interval);
}
}, [api, selectedAccount, isApiReady]);
const loadPresaleData = async () => {
if (!api) return;
try {
// Check if presale active
const isActive = await api.query.presale.presaleActive();
setActive(isActive.toHuman() as boolean);
// Check if paused
const isPaused = await api.query.presale.paused();
setPaused(isPaused.toHuman() as boolean);
if (!isActive.toHuman()) return;
// Get start block and calculate time remaining
const startBlock = await api.query.presale.presaleStartBlock();
const currentBlock = await api.query.system.number();
if (startBlock.isSome) {
const start = startBlock.unwrap().toNumber();
const current = currentBlock.toNumber();
const duration = 45 * 24 * 60 * 10; // 45 days in blocks (6s blocks)
const end = start + duration;
const remaining = Math.max(0, end - current);
setTimeRemaining(remaining * 6); // blocks to seconds
}
// Get total raised
const raised = await api.query.presale.totalRaised();
const raisedValue = raised.toString();
setTotalRaised((parseInt(raisedValue) / 1_000_000).toFixed(2)); // 6 decimals to USDT
// Get contributors count
const contributors = await api.query.presale.contributors();
const contributorsList = contributors.toHuman() as string[];
setContributorsCount(contributorsList?.length || 0);
// Get my contribution if wallet connected
if (selectedAccount) {
const contribution = await api.query.presale.contributions(selectedAccount.address);
const contributionValue = contribution.toString();
setMyContribution((parseInt(contributionValue) / 1_000_000).toFixed(2));
}
} catch (error) {
if (import.meta.env.DEV) console.error('Error loading presale data:', error);
}
};
const handleContribute = async () => {
if (!api || !selectedAccount) {
toast.error('Please connect your wallet first');
return;
}
if (!wusdtAmount || parseFloat(wusdtAmount) <= 0) {
toast.error('Please enter a valid amount');
return;
}
const amount = parseFloat(wusdtAmount);
const wusdtBalance = parseFloat(balances.USDT);
if (amount > wusdtBalance) {
toast.error(`Insufficient wUSDT balance. You have ${wusdtBalance} wUSDT`);
return;
}
setLoading(true);
try {
const amountWithDecimals = Math.floor(amount * 1_000_000); // 6 decimals
const tx = api.tx.presale.contribute(amountWithDecimals);
await tx.signAndSend(selectedAccount.address, ({ status, events }) => {
if (status.isInBlock) {
if (import.meta.env.DEV) {
console.log(`Transaction included in block: ${status.asInBlock}`);
}
// Check for errors
events.forEach(({ event }) => {
if (api.events.system.ExtrinsicFailed.is(event)) {
const [dispatchError] = event.data;
let errorMsg = 'Transaction failed';
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);
errorMsg = `${decoded.section}.${decoded.name}: ${decoded.docs}`;
}
toast.error(errorMsg);
setLoading(false);
} else if (api.events.presale?.Contributed?.is(event)) {
toast.success(`Successfully contributed ${amount} wUSDT!`);
setWusdtAmount('');
loadPresaleData();
setLoading(false);
}
});
}
});
} catch (error) {
if (import.meta.env.DEV) console.error('Contribution error:', error);
toast.error('Contribution failed. Please try again.');
setLoading(false);
}
};
const formatTime = (seconds: number): string => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return `${days}d ${hours}h ${minutes}m`;
};
const calculatePezReceived = (wusdtAmount: string): number => {
const amount = parseFloat(wusdtAmount);
return isNaN(amount) ? 0 : amount * 100; // 1 wUSDT = 100 PEZ
};
const progressPercentage = () => {
const target = 1_000_000; // Target: $1M
const current = parseFloat(totalRaised);
return Math.min(100, (current / target) * 100);
};
if (!active) {
return (
<div className="container mx-auto py-12 px-4">
<Card className="p-8 text-center max-w-2xl mx-auto">
<div className="mb-6">
<div className="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-4">
<Timer className="w-10 h-10 text-green-500" />
</div>
<h1 className="text-3xl font-bold mb-4">PEZ Token Pre-Sale</h1>
<p className="text-muted-foreground">
{t('presale.notStarted', 'The pre-sale has not started yet. Please check back soon!')}
</p>
</div>
<div className="p-6 bg-muted rounded-lg mt-6">
<h3 className="font-semibold mb-3">Pre-Sale Details</h3>
<div className="space-y-2 text-sm text-left">
<div className="flex justify-between">
<span className="text-muted-foreground">Duration:</span>
<span className="font-medium">45 Days</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Conversion Rate:</span>
<span className="font-medium">1 wUSDT = 100 PEZ</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Accepted Token:</span>
<span className="font-medium">wUSDT</span>
</div>
</div>
</div>
</Card>
</div>
);
}
return (
<div className="container mx-auto py-12 px-4">
<div className="max-w-6xl mx-auto">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold mb-2 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
PEZ Token Pre-Sale
</h1>
<p className="text-muted-foreground">
Contribute wUSDT and receive PEZ tokens at a special rate
</p>
</div>
{paused && (
<Alert className="mb-6 border-yellow-500/50 bg-yellow-500/10">
<AlertCircle className="h-4 w-4 text-yellow-500" />
<AlertDescription className="text-yellow-600 dark:text-yellow-400">
Pre-sale is temporarily paused. Contributions are disabled.
</AlertDescription>
</Alert>
)}
{/* Stats Grid */}
<div className="grid md:grid-cols-4 gap-4 mb-8">
<Card className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm text-muted-foreground">Time Remaining</h3>
<Timer className="h-4 w-4 text-green-500" />
</div>
<p className="text-2xl font-bold text-green-400">{formatTime(timeRemaining)}</p>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm text-muted-foreground">Total Raised</h3>
<TrendingUp className="h-4 w-4 text-yellow-500" />
</div>
<p className="text-2xl font-bold text-yellow-400">${totalRaised}</p>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm text-muted-foreground">Contributors</h3>
<Users className="h-4 w-4 text-blue-500" />
</div>
<p className="text-2xl font-bold text-blue-400">{contributorsCount}</p>
</Card>
<Card className="p-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm text-muted-foreground">Your Contribution</h3>
<CheckCircle2 className="h-4 w-4 text-green-500" />
</div>
<p className="text-2xl font-bold">${myContribution}</p>
</Card>
</div>
{/* Progress Bar */}
<Card className="p-6 mb-8">
<div className="flex justify-between items-center mb-2">
<h3 className="text-sm font-medium">Fundraising Progress</h3>
<span className="text-sm text-muted-foreground">{progressPercentage().toFixed(1)}%</span>
</div>
<Progress value={progressPercentage()} className="h-3" />
<p className="text-xs text-muted-foreground mt-2">
Target: $1,000,000 USDT
</p>
</Card>
{/* Contribution Form */}
<Card className="p-8 max-w-2xl mx-auto">
<h2 className="text-2xl font-bold mb-6">Contribute to Pre-Sale</h2>
{!selectedAccount ? (
<Alert className="border-blue-500/50 bg-blue-500/10">
<AlertCircle className="h-4 w-4 text-blue-500" />
<AlertDescription className="text-blue-600 dark:text-blue-400">
Please connect your PezkuwiChain wallet to participate in the pre-sale.
</AlertDescription>
</Alert>
) : (
<>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">Your wUSDT Balance</label>
<div className="p-3 bg-muted rounded-lg">
<span className="text-lg font-semibold">{balances.USDT} wUSDT</span>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-2">wUSDT Amount</label>
<Input
type="number"
value={wusdtAmount}
onChange={(e) => setWusdtAmount(e.target.value)}
placeholder="100"
min="0"
step="0.01"
disabled={paused || loading}
className="text-lg"
/>
<p className="text-sm text-muted-foreground mt-2">
You will receive: <span className="font-semibold text-green-500">{calculatePezReceived(wusdtAmount).toLocaleString()} PEZ</span>
</p>
</div>
<Button
onClick={handleContribute}
className="w-full"
size="lg"
disabled={!selectedAccount || !wusdtAmount || paused || loading}
>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{loading ? 'Contributing...' : 'Contribute wUSDT'}
</Button>
<div className="mt-6 p-4 bg-muted rounded-lg">
<h3 className="font-semibold mb-3 flex items-center">
<CheckCircle2 className="w-4 h-4 mr-2 text-green-500" />
Pre-Sale Terms
</h3>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Conversion Rate:</span>
<span className="font-medium">1 wUSDT = 100 PEZ</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Distribution:</span>
<span className="font-medium">After 45 days</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Lock Period:</span>
<span className="font-medium">None</span>
</div>
</div>
</div>
{parseFloat(balances.USDT) === 0 && (
<Alert className="mt-4 border-yellow-500/50 bg-yellow-500/10">
<AlertCircle className="h-4 w-4 text-yellow-500" />
<AlertDescription className="text-yellow-600 dark:text-yellow-400">
You don't have wUSDT. Please bridge USDT to wUSDT first.
</AlertDescription>
</Alert>
)}
</>
)}
</Card>
{/* Info Cards */}
<div className="grid md:grid-cols-2 gap-6 mt-8 max-w-4xl mx-auto">
<Card className="p-6">
<h3 className="font-semibold mb-3">How to Participate</h3>
<ol className="space-y-2 text-sm list-decimal list-inside">
<li>Connect your PezkuwiChain wallet</li>
<li>Ensure you have wUSDT (bridge if needed)</li>
<li>Enter the amount you want to contribute</li>
<li>Confirm the transaction</li>
<li>Receive PEZ after 45 days</li>
</ol>
</Card>
<Card className="p-6">
<h3 className="font-semibold mb-3">Important Notes</h3>
<ul className="space-y-2 text-sm list-disc list-inside text-muted-foreground">
<li>Minimum contribution: 1 wUSDT</li>
<li>PEZ will be distributed automatically after presale ends</li>
<li>Contributions are final and non-refundable</li>
<li>Pre-sale duration: 45 days</li>
</ul>
</Card>
</div>
</div>
</div>
);
}