feat(web): add network subpages and subdomains listing page

- Add /subdomains page listing all 20 PezkuwiChain subdomains
- Add Back to Home button to Subdomains page
- Create NetworkPage reusable component for network details
- Add 7 network subpages: /mainnet, /staging, /testnet, /beta, /alfa, /development, /local
- Update ChainSpecs network cards to navigate to network subpages
- Add i18n translations for chainSpecs section in en.ts
- Add SDK docs with rebranding support (rebrand-rustdoc.cjs)
- Add generate-docs-structure.cjs for automatic docs generation
- Update shared libs: endpoints, polkadot, wallet, xcm-bridge
- Add new token logos: TYR, ZGR, pezkuwi_icon
- Add new pages: Explorer, Docs, Wallet, Api, Faucet, Developers, Grants, Wiki, Forum, Telemetry
This commit is contained in:
2025-12-11 00:33:47 +03:00
parent 2c6c4f5606
commit 11678fe7cd
976 changed files with 60601 additions and 168 deletions
+3 -4
View File
@@ -266,8 +266,7 @@ const AppLayout: React.FC = () => {
)}
<a
href="https://raw.githubusercontent.com/pezkuwichain/DKSweb/main/public/Whitepaper.pdf"
download="Pezkuwi_Whitepaper.pdf"
href="/docs"
className="text-gray-300 hover:text-white transition-colors text-sm"
>
Docs
@@ -518,13 +517,13 @@ const AppLayout: React.FC = () => {
<h4 className="text-white font-semibold mb-4 text-left">Developers</h4>
<ul className="space-y-2 text-left">
<li>
<a href="https://explorer.pezkuwichain.io" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
<a href="/api" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
API
<ExternalLink className="w-3 h-3 ml-1" />
</a>
</li>
<li>
<a href="https://sdk.pezkuwichain.io" target="_blank" rel="noopener noreferrer" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
<a href="/developers" className="text-gray-400 hover:text-white text-sm inline-flex items-center">
SDK
<ExternalLink className="w-3 h-3 ml-1" />
</a>
+72 -60
View File
@@ -1,5 +1,9 @@
import React, { useState } from 'react';
import { Server, Globe, TestTube, Code, Wifi, Copy, Check } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Server, Globe, TestTube, Code, Wifi, Copy, Check, ExternalLink, Compass, Book, Briefcase, FileCode, HandCoins, Users, Wrench, MessageCircle, GitFork } from 'lucide-react';
// ... (interface and const arrays remain the same) ...
interface ChainSpec {
id: string;
@@ -58,6 +62,17 @@ const chainSpecs: ChainSpec[] = [
features: ['Experimental', 'New Features', 'Limited Access'],
color: 'from-orange-500 to-orange-600'
},
{
id: 'alfa',
name: 'PezkuwiChain Alfa Testnet',
type: 'Development',
icon: <TestTube className="w-5 h-5" />,
endpoint: 'ws://127.0.0.1:8844',
chainId: 'pezkuwichain_alfa_testnet',
validators: 4,
features: ['4 Validators', 'Staking Active', 'Full Features'],
color: 'from-purple-500 to-pink-600'
},
{
id: 'development',
name: 'Development',
@@ -82,9 +97,24 @@ const chainSpecs: ChainSpec[] = [
}
];
const subdomains = [
{ name: 'Explorer', href: '/explorer', icon: <Compass />, external: false },
{ name: 'Docs', href: '/docs', icon: <Book />, external: false },
{ name: 'Wallet', href: '/wallet', icon: <Briefcase />, external: false },
{ name: 'API', href: '/api', icon: <FileCode />, external: false },
{ name: 'Faucet', href: '/faucet', icon: <HandCoins />, external: false },
{ name: 'Developers', href: '/developers', icon: <Users />, external: false },
{ name: 'Grants', href: '/grants', icon: <Wrench />, external: false },
{ name: 'Wiki', href: '/wiki', icon: <MessageCircle />, external: false },
{ name: 'Forum', href: '/forum', icon: <GitFork />, external: false },
{ name: 'Telemetry', href: '/telemetry', icon: <Server />, external: false },
]
const ChainSpecs: React.FC = () => {
const { t } = useTranslation();
const [copiedId, setCopiedId] = useState<string | null>(null);
const [selectedSpec, setSelectedSpec] = useState<ChainSpec>(chainSpecs[0]);
const navigate = useNavigate();
const copyToClipboard = (text: string, id: string) => {
navigator.clipboard.writeText(text);
@@ -97,18 +127,18 @@ const ChainSpecs: React.FC = () => {
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-12">
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-400 to-cyan-400 bg-clip-text text-transparent">
Chain Specifications
{t('chainSpecs.title')}
</h2>
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
Multiple network environments for development, testing, and production
{t('chainSpecs.subtitle')}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{chainSpecs.map((spec) => (
<div
key={spec.id}
onClick={() => setSelectedSpec(spec)}
onClick={() => navigate(`/${spec.id}`)}
className={`cursor-pointer p-4 rounded-xl border transition-all ${
selectedSpec.id === spec.id
? 'bg-gray-900 border-purple-500'
@@ -136,6 +166,26 @@ const ChainSpecs: React.FC = () => {
</div>
</div>
))}
{/* Subdomains Box */}
<div
onClick={() => navigate('/subdomains')}
className="md:col-span-2 lg:col-span-1 cursor-pointer p-4 rounded-xl border transition-all bg-gray-950/50 border-gray-800 hover:border-gray-700"
>
<div className="flex items-start justify-between mb-3">
<div className="p-2 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 bg-opacity-20">
<Compass className="w-5 h-5" />
</div>
<span className="px-2 py-1 text-xs rounded-full bg-gray-900/30 text-gray-400">
{t('chainSpecs.services')}
</span>
</div>
<h3 className="text-white font-semibold mb-2">{t('chainSpecs.subdomainsTitle')}</h3>
<div className="flex items-center text-sm text-gray-400">
<ExternalLink className="w-3 h-3 mr-1" />
<span>{t('chainSpecs.availableServices', { count: subdomains.length })}</span>
</div>
</div>
</div>
{/* Selected Chain Details */}
@@ -151,7 +201,7 @@ const ChainSpecs: React.FC = () => {
<div className="space-y-4">
<div>
<label className="text-gray-400 text-sm">WebSocket Endpoint</label>
<label className="text-gray-400 text-sm">{t('chainSpecs.websocketEndpoint')}</label>
<div className="flex items-center mt-1">
<code className="flex-1 p-3 bg-gray-900 rounded-lg text-cyan-400 font-mono text-sm">
{selectedSpec.endpoint}
@@ -169,7 +219,7 @@ const ChainSpecs: React.FC = () => {
</div>
<div>
<label className="text-gray-400 text-sm">Chain ID</label>
<label className="text-gray-400 text-sm">{t('chainSpecs.chainId')}</label>
<div className="flex items-center mt-1">
<code className="flex-1 p-3 bg-gray-900 rounded-lg text-purple-400 font-mono text-sm">
{selectedSpec.chainId}
@@ -187,64 +237,26 @@ const ChainSpecs: React.FC = () => {
</div>
<div>
<label className="text-gray-400 text-sm mb-2 block">Features</label>
<div className="flex flex-wrap gap-2">
{selectedSpec.features.map((feature) => (
<span
key={feature}
className="px-3 py-1 bg-gray-900 text-gray-300 text-sm rounded-full"
>
{feature}
</span>
))}
</div>
<button
onClick={() => navigate('/explorer')}
className="w-full mt-2 bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center transition-colors"
>
<Compass className="w-4 h-4 mr-2" />
{t('chainSpecs.viewExplorer')}
</button>
</div>
</div>
</div>
<div>
<h4 className="text-lg font-semibold text-white mb-4">Connection Example</h4>
<div className="bg-gray-900 rounded-lg p-4 font-mono text-sm">
<div className="text-gray-400 mb-2">{`// Using @polkadot/api`}</div>
<div className="text-cyan-400">import</div>
<div className="text-white ml-2">{'{ ApiPromise, WsProvider }'}</div>
<div className="text-cyan-400">from</div>
<div className="text-green-400 mb-3">&apos;@polkadot/api&apos;;</div>
<div className="text-cyan-400">const</div>
<div className="text-white ml-2">provider =</div>
<div className="text-cyan-400 ml-2">new</div>
<div className="text-yellow-400 ml-2">WsProvider(</div>
<div className="text-green-400 ml-4">&apos;{selectedSpec.endpoint}&apos;</div>
<div className="text-yellow-400">);</div>
<div className="text-cyan-400 mt-2">const</div>
<div className="text-white ml-2">api =</div>
<div className="text-cyan-400 ml-2">await</div>
<div className="text-yellow-400 ml-2">ApiPromise.create</div>
<div className="text-white">({'{ provider }'});</div>
</div>
<div className="mt-4 p-4 bg-kurdish-green/20 rounded-lg border border-kurdish-green/30">
<h5 className="text-kurdish-green font-semibold mb-2">Network Stats</h5>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-gray-400">Block Time:</span>
<span className="text-white ml-2">6s</span>
</div>
<div>
<span className="text-gray-400">Finality:</span>
<span className="text-white ml-2">GRANDPA</span>
</div>
<div>
<span className="text-gray-400">Consensus:</span>
<span className="text-white ml-2">Aura</span>
</div>
<div>
<span className="text-gray-400">Runtime:</span>
<span className="text-white ml-2">v1.0.0</span>
</div>
</div>
<h4 className="text-lg font-semibold text-white mb-4">{t('chainSpecs.availableSubdomains')}</h4>
<div className="grid grid-cols-2 gap-4">
{subdomains.map(subdomain => (
<div key={subdomain.name} onClick={() => navigate(subdomain.href)} className="flex items-center p-3 bg-gray-900 rounded-lg cursor-pointer hover:bg-gray-800 transition-colors">
<div className="mr-3 text-cyan-400">{subdomain.icon}</div>
<span className="font-semibold">{subdomain.name}</span>
</div>
))}
</div>
</div>
</div>
+67
View File
@@ -0,0 +1,67 @@
import React from 'react';
import { Link, NavLink } from 'react-router-dom';
const PezkuwiChainLogo: React.FC = () => {
return (
<img src="/PezkuwiChain_Logo_Horizontal_Green_White.png" alt="PezkuwiChain Logo" className="h-8" />
);
};
const Header: React.FC = () => {
const linkStyle = "text-white hover:text-green-400 transition-colors";
const activeLinkStyle = { color: '#34D399' }; // green-400
return (
<header className="bg-gray-900 text-white p-4 fixed top-0 left-0 right-0 z-[1000]">
<div className="container mx-auto flex justify-between items-center">
<Link to="/">
<PezkuwiChainLogo />
</Link>
<nav>
<ul className="flex space-x-4">
<li><NavLink to="/explorer" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Explorer</NavLink></li>
<li><NavLink to="/docs" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Docs</NavLink></li>
<li><NavLink to="/wallet" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Wallet</NavLink></li>
<li><NavLink to="/api" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>API</NavLink></li>
<li><NavLink to="/faucet" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Faucet</NavLink></li>
<li><NavLink to="/developers" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Developers</NavLink></li>
<li><NavLink to="/grants" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Grants</NavLink></li>
<li><NavLink to="/wiki" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Wiki</NavLink></li>
<li><NavLink to="/forum" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Forum</NavLink></li>
<li><NavLink to="/telemetry" className={linkStyle} style={({ isActive }) => isActive ? activeLinkStyle : undefined}>Telemetry</NavLink></li>
</ul>
</nav>
</div>
</header>
);
};
const Footer: React.FC = () => {
return (
<footer className="bg-gray-900 text-white p-4">
<div className="container mx-auto text-center">
<p>&copy; {new Date().getFullYear()} PezkuwiChain. All rights reserved.</p>
</div>
</footer>
);
};
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<div className="flex flex-col min-h-screen">
<Header />
<div className="flex-grow overflow-auto pt-16"> {/* Add padding-top equal to header height */}
<main className="container mx-auto p-4">
{children}
</main>
</div>
<Footer />
</div>
);
};
export default Layout;
+57 -10
View File
@@ -10,6 +10,7 @@ export const NetworkStats: React.FC = () => {
const [blockHash, setBlockHash] = useState<string>('');
const [finalizedBlock, setFinalizedBlock] = useState<number>(0);
const [validatorCount, setValidatorCount] = useState<number>(0);
const [collatorCount, setCollatorCount] = useState<number>(0);
const [nominatorCount, setNominatorCount] = useState<number>(0);
const [peers, setPeers] = useState<number>(0);
@@ -33,23 +34,51 @@ export const NetworkStats: React.FC = () => {
setFinalizedBlock(header.number.toNumber());
});
// Update validator count, nominator count, and peer count every 3 seconds
// Update validator count, collator count, nominator count, and peer count every 3 seconds
const updateNetworkStats = async () => {
try {
const validators = await api.query.session.validators();
const health = await api.rpc.system.health();
// Count nominators
let nominatorCount = 0;
// 1. Fetch Validators
let vCount = 0;
try {
const nominators = await api.query.staking.nominators.entries();
nominatorCount = nominators.length;
if (api.query.session?.validators) {
const validators = await api.query.session.validators();
if (validators) {
vCount = validators.length;
}
}
} catch (err) {
if (import.meta.env.DEV) console.warn('Failed to fetch validators', err);
}
// 2. Fetch Collators (Invulnerables)
let cCount = 0;
try {
if (api.query.collatorSelection?.invulnerables) {
const invulnerables = await api.query.collatorSelection.invulnerables();
if (invulnerables) {
cCount = invulnerables.length;
}
}
} catch (err) {
if (import.meta.env.DEV) console.warn('Failed to fetch collators', err);
}
// 3. Count Nominators
let nCount = 0;
try {
const nominators = await api.query.staking?.nominators.entries();
if (nominators) {
nCount = nominators.length;
}
} catch {
if (import.meta.env.DEV) console.warn('Staking pallet not available, nominators = 0');
}
setValidatorCount(validators.length);
setNominatorCount(nominatorCount);
setValidatorCount(vCount);
setCollatorCount(cCount);
setNominatorCount(nCount);
setPeers(health.peers.toNumber());
} catch (err) {
if (import.meta.env.DEV) console.error('Failed to update network stats:', err);
@@ -109,7 +138,7 @@ export const NetworkStats: React.FC = () => {
}
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
{/* Connection Status */}
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
@@ -179,7 +208,25 @@ export const NetworkStats: React.FC = () => {
{validatorCount}
</div>
<div className="text-xs text-gray-500 mt-1">
Securing the network - LIVE
Validating blocks
</div>
</CardContent>
</Card>
{/* Collators */}
<Card className="bg-gray-900 border-gray-800">
<CardHeader className="pb-3">
<CardTitle className="text-sm font-medium text-gray-400 flex items-center gap-2">
<Users className="w-4 h-4 text-orange-500" />
Active Collators
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">
{collatorCount}
</div>
<div className="text-xs text-gray-500 mt-1">
Producing blocks
</div>
</CardContent>
</Card>
+28 -9
View File
@@ -54,7 +54,7 @@ const PoolDashboard = () => {
// Pool selection state
const [availablePools, setAvailablePools] = useState<Array<[number, number]>>([]);
const [selectedPool, setSelectedPool] = useState<string>('1-2'); // Default: PEZ/wUSDT
const [selectedPool, setSelectedPool] = useState<string>('0-1'); // Default: wHEZ/PEZ
// Discover available pools
useEffect(() => {
@@ -62,25 +62,44 @@ const PoolDashboard = () => {
const discoverPools = async () => {
try {
// Check all possible pool combinations
// Check all possible pool combinations in both directions
// Pools can be stored as [A,B] or [B,A] depending on creation order
// Note: .env sets WUSDT to Asset ID based on VITE_ASSET_WUSDT
const possiblePools: Array<[number, number]> = [
[ASSET_IDS.WHEZ, ASSET_IDS.PEZ], // wHEZ/PEZ
[ASSET_IDS.WHEZ, ASSET_IDS.WUSDT], // wHEZ/wUSDT
[ASSET_IDS.PEZ, ASSET_IDS.WUSDT], // PEZ/wUSDT
[ASSET_IDS.WHEZ, ASSET_IDS.PEZ], // wHEZ(0) / PEZ(1) -> Shows as HEZ-PEZ
[ASSET_IDS.PEZ, ASSET_IDS.WHEZ], // PEZ(1) / wHEZ(0) -> Shows as HEZ-PEZ (reverse)
[ASSET_IDS.WHEZ, ASSET_IDS.WUSDT], // wHEZ(0) / wUSDT -> Shows as HEZ-USDT
[ASSET_IDS.WUSDT, ASSET_IDS.WHEZ], // wUSDT / wHEZ(0) -> Shows as HEZ-USDT (reverse)
[ASSET_IDS.PEZ, ASSET_IDS.WUSDT], // PEZ(1) / wUSDT -> Shows as PEZ-USDT
[ASSET_IDS.WUSDT, ASSET_IDS.PEZ], // wUSDT / PEZ(1) -> Shows as PEZ-USDT (reverse)
];
const existingPools: Array<[number, number]> = [];
for (const [asset0, asset1] of possiblePools) {
const poolInfo = await api.query.assetConversion.pools([asset0, asset1]);
if (poolInfo.isSome) {
existingPools.push([asset0, asset1]);
try {
const poolInfo = await api.query.assetConversion.pools([asset0, asset1]);
if (poolInfo.isSome) {
existingPools.push([asset0, asset1]);
if (import.meta.env.DEV) {
console.log(`✅ Found pool: ${asset0}-${asset1}`);
}
}
} catch (err) {
// Skip pools that error out (likely don't exist)
if (import.meta.env.DEV) {
console.log(`❌ Pool ${asset0}-${asset1} not found or error:`, err);
}
}
}
if (import.meta.env.DEV) {
console.log('📊 Total pools found:', existingPools.length, existingPools);
}
setAvailablePools(existingPools);
// Set default pool to first available if current selection doesn&apos;t exist
// Set default pool to first available if current selection doesn't exist
if (existingPools.length > 0) {
const currentPoolKey = selectedPool;
const poolExists = existingPools.some(
+5 -5
View File
@@ -430,8 +430,8 @@ const TokenSwap = () => {
if (import.meta.env.DEV) console.warn('Failed to parse swap path:', err);
}
const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : fromAssetId === 2 ? 'USDT' : `Asset${fromAssetId}`;
const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : toAssetId === 2 ? 'USDT' : `Asset${toAssetId}`;
const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : fromAssetId === 1000 ? 'USDT' : `Asset${fromAssetId}`;
const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : toAssetId === 1000 ? 'USDT' : `Asset${toAssetId}`;
// Only show transactions from current user
if (who.toString() === selectedAccount.address) {
@@ -712,7 +712,7 @@ const TokenSwap = () => {
if (Array.isArray(pathArray) && pathArray.length >= 2) {
const asset0 = pathArray[0];
//const _asset1 = pathArray[1];
const asset1 = pathArray[1];
// Each element is a tuple where index 0 is the asset ID
if (Array.isArray(asset0) && asset0.length >= 1) {
@@ -726,8 +726,8 @@ const TokenSwap = () => {
if (import.meta.env.DEV) console.warn('Failed to parse swap path in refresh:', err);
}
const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : fromAssetId === 2 ? 'USDT' : `Asset${fromAssetId}`;
const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : toAssetId === 2 ? 'USDT' : `Asset${toAssetId}`;
const fromTokenSymbol = fromAssetId === 0 ? 'wHEZ' : fromAssetId === 1 ? 'PEZ' : fromAssetId === 1000 ? 'USDT' : `Asset${fromAssetId}`;
const toTokenSymbol = toAssetId === 0 ? 'wHEZ' : toAssetId === 1 ? 'PEZ' : toAssetId === 1000 ? 'USDT' : `Asset${toAssetId}`;
if (who.toString() === selectedAccount.address) {
transactions.push({
@@ -0,0 +1,875 @@
/**
* XCM Configuration Wizard - Multi-Step Parachain Setup
*
* Guides admin through complete XCM integration:
* 1. Reserve ParaId
* 2. Generate Chain Artifacts (genesis + WASM)
* 3. Register Parachain
* 4. Open HRMP Channels
* 5. Register Foreign Assets
* 6. Test XCM Transfer
*/
import React, { useState, useEffect } from 'react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { useWallet } from '@/contexts/WalletContext';
import {
X,
CheckCircle,
Circle,
Loader2,
AlertCircle,
Download,
Upload,
Send,
Link2,
Coins,
TestTube,
ChevronRight,
} from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { useToast } from '@/hooks/use-toast';
import {
reserveParaId,
generateChainArtifacts,
registerParachain,
openHRMPChannels,
registerForeignAssets,
testXCMTransfer,
getRelayChainEndpoint,
getAssetHubParaId,
type RelayChain,
type ChainArtifacts,
type HRMPChannel,
type RegisteredAsset,
type ForeignAsset,
} from '@pezkuwi/lib/xcm-wizard';
import { ApiPromise, WsProvider } from '@polkadot/api';
interface XCMConfigurationWizardProps {
isOpen: boolean;
onClose: () => void;
onSuccess?: () => void;
}
interface StepStatus {
completed: boolean;
data?: any;
error?: string;
}
export const XCMConfigurationWizard: React.FC<XCMConfigurationWizardProps> = ({
isOpen,
onClose,
onSuccess,
}) => {
const { api, isApiReady } = usePolkadot();
const { account, signer } = useWallet();
const { toast } = useToast();
// Wizard state
const [currentStep, setCurrentStep] = useState<number>(1);
const [steps, setSteps] = useState<Record<number, StepStatus>>({
1: { completed: false },
2: { completed: false },
3: { completed: false },
4: { completed: false },
5: { completed: false },
6: { completed: false },
});
// Step 1: Reserve ParaId
const [relayChain, setRelayChain] = useState<RelayChain>('westend');
const [reservedParaId, setReservedParaId] = useState<number | null>(null);
const [reserving, setReserving] = useState(false);
// Step 2: Generate Artifacts
const [artifacts, setArtifacts] = useState<ChainArtifacts | null>(null);
const [generating, setGenerating] = useState(false);
// Step 3: Register Parachain
const [genesisFile, setGenesisFile] = useState<File | null>(null);
const [wasmFile, setWasmFile] = useState<File | null>(null);
const [registering, setRegistering] = useState(false);
const [registrationTxHash, setRegistrationTxHash] = useState<string>('');
// Step 4: HRMP Channels
const [openingChannels, setOpeningChannels] = useState(false);
const [openedChannels, setOpenedChannels] = useState<HRMPChannel[]>([]);
// Step 5: Foreign Assets
const [registeringAssets, setRegisteringAssets] = useState(false);
const [registeredAssets, setRegisteredAssets] = useState<RegisteredAsset[]>([]);
// Step 6: XCM Test
const [testing, setTesting] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; balance: string } | null>(null);
const totalSteps = 6;
const progress = (Object.values(steps).filter(s => s.completed).length / totalSteps) * 100;
// Reset state when modal opens/closes
useEffect(() => {
if (!isOpen) {
setCurrentStep(1);
setSteps({
1: { completed: false },
2: { completed: false },
3: { completed: false },
4: { completed: false },
5: { completed: false },
6: { completed: false },
});
setReservedParaId(null);
setArtifacts(null);
setOpenedChannels([]);
setRegisteredAssets([]);
setTestResult(null);
}
}, [isOpen]);
// ========================================
// STEP 1: RESERVE PARAID
// ========================================
const handleReserveParaId = async () => {
if (!account || !signer) {
toast({
title: 'Wallet not connected',
description: 'Please connect your wallet first',
variant: 'destructive',
});
return;
}
setReserving(true);
try {
// Connect to relay chain
const endpoint = getRelayChainEndpoint(relayChain);
const provider = new WsProvider(endpoint);
const relayApi = await ApiPromise.create({ provider });
// Reserve ParaId
const paraId = await reserveParaId(relayApi, relayChain, account);
setReservedParaId(paraId);
// Mark step as completed
setSteps(prev => ({
...prev,
1: { completed: true, data: { paraId, relayChain } },
}));
toast({
title: 'ParaId Reserved!',
description: `Successfully reserved ParaId ${paraId} on ${relayChain}`,
});
// Auto-advance to next step
setCurrentStep(2);
await relayApi.disconnect();
} catch (error) {
console.error('Failed to reserve ParaId:', error);
setSteps(prev => ({
...prev,
1: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
}));
toast({
title: 'Reservation Failed',
description: error instanceof Error ? error.message : 'Failed to reserve ParaId',
variant: 'destructive',
});
} finally {
setReserving(false);
}
};
// ========================================
// STEP 2: GENERATE CHAIN ARTIFACTS
// ========================================
const handleGenerateArtifacts = async () => {
if (!reservedParaId) return;
setGenerating(true);
try {
const chainName = `pezkuwichain-${relayChain}`;
const artifactData = await generateChainArtifacts(chainName);
setArtifacts(artifactData);
setSteps(prev => ({
...prev,
2: { completed: true, data: artifactData },
}));
toast({
title: 'Artifacts Generated!',
description: 'Genesis state and runtime WASM are ready for download',
});
setCurrentStep(3);
} catch (error) {
console.error('Failed to generate artifacts:', error);
setSteps(prev => ({
...prev,
2: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
}));
toast({
title: 'Generation Failed',
description: 'Failed to generate chain artifacts',
variant: 'destructive',
});
} finally {
setGenerating(false);
}
};
// ========================================
// STEP 3: REGISTER PARACHAIN
// ========================================
const handleRegisterParachain = async () => {
if (!reservedParaId || !genesisFile || !wasmFile || !account || !signer) {
toast({
title: 'Missing Data',
description: 'Please upload both genesis and WASM files',
variant: 'destructive',
});
return;
}
setRegistering(true);
try {
const endpoint = getRelayChainEndpoint(relayChain);
const provider = new WsProvider(endpoint);
const relayApi = await ApiPromise.create({ provider });
const txHash = await registerParachain(relayApi, reservedParaId, genesisFile, wasmFile, account);
setRegistrationTxHash(txHash);
setSteps(prev => ({
...prev,
3: { completed: true, data: { txHash, paraId: reservedParaId } },
}));
toast({
title: 'Parachain Registered!',
description: `ParaId ${reservedParaId} registered on ${relayChain}`,
});
setCurrentStep(4);
await relayApi.disconnect();
} catch (error) {
console.error('Failed to register parachain:', error);
setSteps(prev => ({
...prev,
3: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
}));
toast({
title: 'Registration Failed',
description: error instanceof Error ? error.message : 'Failed to register parachain',
variant: 'destructive',
});
} finally {
setRegistering(false);
}
};
// ========================================
// STEP 4: OPEN HRMP CHANNELS
// ========================================
const handleOpenHRMPChannels = async () => {
if (!reservedParaId || !account || !signer) return;
setOpeningChannels(true);
try {
const endpoint = getRelayChainEndpoint(relayChain);
const provider = new WsProvider(endpoint);
const relayApi = await ApiPromise.create({ provider });
// Get Asset Hub ParaId
const assetHubParaId = getAssetHubParaId(relayChain);
// Open channels with Asset Hub
const channels = await openHRMPChannels(relayApi, reservedParaId, [assetHubParaId], account);
setOpenedChannels(channels);
setSteps(prev => ({
...prev,
4: { completed: true, data: { channels } },
}));
toast({
title: 'HRMP Channels Opened!',
description: `Opened ${channels.length} channel(s) with Asset Hub`,
});
setCurrentStep(5);
await relayApi.disconnect();
} catch (error) {
console.error('Failed to open HRMP channels:', error);
setSteps(prev => ({
...prev,
4: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
}));
toast({
title: 'Channel Opening Failed',
description: error instanceof Error ? error.message : 'Failed to open HRMP channels',
variant: 'destructive',
});
} finally {
setOpeningChannels(false);
}
};
// ========================================
// STEP 5: REGISTER FOREIGN ASSETS
// ========================================
const handleRegisterAssets = async () => {
if (!api || !isApiReady || !account || !signer) return;
setRegisteringAssets(true);
try {
// Define foreign assets to register (USDT, DOT, etc.)
const foreignAssets: ForeignAsset[] = [
{
symbol: 'USDT',
location: {
parents: 1,
interior: {
X3: [{ Parachain: 1000 }, { PalletInstance: 50 }, { GeneralIndex: 1984 }],
},
},
metadata: {
name: 'Tether USD',
symbol: 'USDT',
decimals: 6,
minBalance: '1000',
},
},
{
symbol: 'DOT',
location: {
parents: 1,
interior: { Here: null },
},
metadata: {
name: 'Polkadot',
symbol: 'DOT',
decimals: 10,
minBalance: '10000000000',
},
},
];
const registered = await registerForeignAssets(api, foreignAssets, account);
setRegisteredAssets(registered);
setSteps(prev => ({
...prev,
5: { completed: true, data: { assets: registered } },
}));
toast({
title: 'Assets Registered!',
description: `Registered ${registered.length} foreign asset(s)`,
});
setCurrentStep(6);
} catch (error) {
console.error('Failed to register assets:', error);
setSteps(prev => ({
...prev,
5: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
}));
toast({
title: 'Asset Registration Failed',
description: error instanceof Error ? error.message : 'Failed to register foreign assets',
variant: 'destructive',
});
} finally {
setRegisteringAssets(false);
}
};
// ========================================
// STEP 6: TEST XCM TRANSFER
// ========================================
const handleTestXCMTransfer = async () => {
if (!api || !isApiReady || !account || !signer) return;
setTesting(true);
try {
const result = await testXCMTransfer(api, '1000000', account); // 1 USDT (6 decimals)
setTestResult(result);
setSteps(prev => ({
...prev,
6: { completed: result.success, data: result },
}));
if (result.success) {
toast({
title: 'XCM Test Successful!',
description: `Received ${result.balance} wUSDT`,
});
} else {
toast({
title: 'XCM Test Failed',
description: result.error || 'Test transfer failed',
variant: 'destructive',
});
}
} catch (error) {
console.error('Failed to test XCM transfer:', error);
setSteps(prev => ({
...prev,
6: { completed: false, error: error instanceof Error ? error.message : 'Unknown error' },
}));
toast({
title: 'Test Failed',
description: error instanceof Error ? error.message : 'XCM test failed',
variant: 'destructive',
});
} finally {
setTesting(false);
}
};
// ========================================
// RENDER STEP CONTENT
// ========================================
const renderStepContent = () => {
switch (currentStep) {
case 1:
return (
<div className="space-y-4">
<div className="space-y-2">
<Label>Select Relay Chain</Label>
<Select value={relayChain} onValueChange={(value: RelayChain) => setRelayChain(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="westend">Westend (Testnet)</SelectItem>
<SelectItem value="rococo">Rococo (Testnet)</SelectItem>
<SelectItem value="polkadot">Polkadot (Mainnet)</SelectItem>
</SelectContent>
</Select>
</div>
{reservedParaId && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
ParaId <strong>{reservedParaId}</strong> reserved on {relayChain}
</AlertDescription>
</Alert>
)}
{steps[1].error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{steps[1].error}</AlertDescription>
</Alert>
)}
<Button onClick={handleReserveParaId} disabled={reserving || steps[1].completed} className="w-full">
{reserving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Reserving ParaId...
</>
) : steps[1].completed ? (
<>
<CheckCircle className="mr-2 h-4 w-4" />
ParaId Reserved
</>
) : (
'Reserve ParaId'
)}
</Button>
</div>
);
case 2:
return (
<div className="space-y-4">
{artifacts && (
<div className="space-y-2">
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
Artifacts generated. Download files for registration.
</AlertDescription>
</Alert>
<div className="grid grid-cols-2 gap-2">
<Button variant="outline" size="sm" asChild>
<a href={artifacts.genesisPath} download>
<Download className="mr-2 h-4 w-4" />
Genesis ({artifacts.genesisSize} bytes)
</a>
</Button>
<Button variant="outline" size="sm" asChild>
<a href={artifacts.wasmPath} download>
<Download className="mr-2 h-4 w-4" />
WASM ({artifacts.wasmSize} bytes)
</a>
</Button>
</div>
</div>
)}
{steps[2].error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{steps[2].error}</AlertDescription>
</Alert>
)}
<Button onClick={handleGenerateArtifacts} disabled={generating || steps[2].completed} className="w-full">
{generating ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Generating Artifacts...
</>
) : steps[2].completed ? (
<>
<CheckCircle className="mr-2 h-4 w-4" />
Artifacts Generated
</>
) : (
'Generate Chain Artifacts'
)}
</Button>
</div>
);
case 3:
return (
<div className="space-y-4">
<div className="space-y-2">
<Label>Upload Genesis State</Label>
<Input
type="file"
accept=".hex,.txt"
onChange={(e) => setGenesisFile(e.target.files?.[0] || null)}
/>
</div>
<div className="space-y-2">
<Label>Upload Runtime WASM</Label>
<Input
type="file"
accept=".wasm"
onChange={(e) => setWasmFile(e.target.files?.[0] || null)}
/>
</div>
{registrationTxHash && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
Registered! TX: <code className="text-xs">{registrationTxHash.slice(0, 20)}...</code>
</AlertDescription>
</Alert>
)}
{steps[3].error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{steps[3].error}</AlertDescription>
</Alert>
)}
<Button
onClick={handleRegisterParachain}
disabled={!genesisFile || !wasmFile || registering || steps[3].completed}
className="w-full"
>
{registering ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Registering Parachain...
</>
) : steps[3].completed ? (
<>
<CheckCircle className="mr-2 h-4 w-4" />
Parachain Registered
</>
) : (
'Register Parachain'
)}
</Button>
</div>
);
case 4:
return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Opening HRMP channels with Asset Hub (ParaId {getAssetHubParaId(relayChain)})
</p>
{openedChannels.length > 0 && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
Opened {openedChannels.length} channel(s):
<ul className="mt-2 space-y-1 text-xs">
{openedChannels.map((ch, idx) => (
<li key={idx}>
{ch.sender} {ch.receiver} (ID: {ch.channelId.slice(0, 10)}...)
</li>
))}
</ul>
</AlertDescription>
</Alert>
)}
{steps[4].error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{steps[4].error}</AlertDescription>
</Alert>
)}
<Button onClick={handleOpenHRMPChannels} disabled={openingChannels || steps[4].completed} className="w-full">
{openingChannels ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Opening HRMP Channels...
</>
) : steps[4].completed ? (
<>
<CheckCircle className="mr-2 h-4 w-4" />
Channels Opened
</>
) : (
'Open HRMP Channels'
)}
</Button>
</div>
);
case 5:
return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Register foreign assets: USDT, DOT, and other cross-chain tokens
</p>
{registeredAssets.length > 0 && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>
Registered {registeredAssets.length} asset(s):
<ul className="mt-2 space-y-1 text-xs">
{registeredAssets.map((asset, idx) => (
<li key={idx}>
{asset.symbol} (Asset ID: {asset.assetId})
</li>
))}
</ul>
</AlertDescription>
</Alert>
)}
{steps[5].error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{steps[5].error}</AlertDescription>
</Alert>
)}
<Button onClick={handleRegisterAssets} disabled={registeringAssets || steps[5].completed} className="w-full">
{registeringAssets ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Registering Assets...
</>
) : steps[5].completed ? (
<>
<CheckCircle className="mr-2 h-4 w-4" />
Assets Registered
</>
) : (
'Register Foreign Assets'
)}
</Button>
</div>
);
case 6:
return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Test XCM transfer from Asset Hub to verify bridge functionality
</p>
{testResult && (
<Alert variant={testResult.success ? 'default' : 'destructive'}>
{testResult.success ? <CheckCircle className="h-4 w-4" /> : <AlertCircle className="h-4 w-4" />}
<AlertDescription>
{testResult.success
? `Test successful! Balance: ${testResult.balance} wUSDT`
: `Test failed: ${testResult.error}`}
</AlertDescription>
</Alert>
)}
{steps[6].error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{steps[6].error}</AlertDescription>
</Alert>
)}
<Button onClick={handleTestXCMTransfer} disabled={testing || steps[6].completed} className="w-full">
{testing ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Testing XCM Transfer...
</>
) : steps[6].completed ? (
<>
<CheckCircle className="mr-2 h-4 w-4" />
XCM Test Passed
</>
) : (
'Test XCM Transfer'
)}
</Button>
</div>
);
default:
return null;
}
};
// Check if all steps are completed
const allStepsCompleted = Object.values(steps).every(s => s.completed);
// Handle Finish Configuration
const handleFinishConfiguration = () => {
toast({
title: 'XCM Configuration Complete!',
description: 'Your parachain is fully configured and ready for cross-chain transfers',
});
if (onSuccess) {
onSuccess();
}
onClose();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
<Card className="w-full max-w-3xl max-h-[90vh] overflow-y-auto">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>XCM Configuration Wizard</CardTitle>
<CardDescription>
Complete parachain setup and cross-chain integration
</CardDescription>
</div>
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-4 w-4" />
</Button>
</div>
<div className="mt-4">
<Progress value={progress} className="h-2" />
<p className="mt-2 text-xs text-muted-foreground text-center">
{Object.values(steps).filter(s => s.completed).length} / {totalSteps} steps completed
</p>
</div>
</CardHeader>
<CardContent className="space-y-6">
{/* Step Navigation */}
<div className="grid grid-cols-6 gap-2">
{[1, 2, 3, 4, 5, 6].map((step) => (
<button
key={step}
onClick={() => setCurrentStep(step)}
className={`flex flex-col items-center gap-1 p-2 rounded-lg transition-colors ${
currentStep === step
? 'bg-kurdish-green text-white'
: steps[step].completed
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-500'
}`}
>
{steps[step].completed ? (
<CheckCircle className="h-5 w-5" />
) : currentStep === step ? (
<Circle className="h-5 w-5 fill-current" />
) : (
<Circle className="h-5 w-5" />
)}
<span className="text-xs font-medium">{step}</span>
</button>
))}
</div>
{/* Current Step Content */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Badge variant="outline">Step {currentStep}</Badge>
<h3 className="font-semibold">
{currentStep === 1 && 'Reserve ParaId'}
{currentStep === 2 && 'Generate Chain Artifacts'}
{currentStep === 3 && 'Register Parachain'}
{currentStep === 4 && 'Open HRMP Channels'}
{currentStep === 5 && 'Register Foreign Assets'}
{currentStep === 6 && 'Test XCM Transfer'}
</h3>
</div>
{renderStepContent()}
</div>
{/* Navigation Buttons */}
<div className="flex items-center justify-between pt-4 border-t">
<Button
variant="outline"
onClick={() => setCurrentStep(Math.max(1, currentStep - 1))}
disabled={currentStep === 1}
>
Previous
</Button>
{allStepsCompleted ? (
<Button onClick={handleFinishConfiguration} className="bg-kurdish-green hover:bg-kurdish-green-dark">
<CheckCircle className="mr-2 h-4 w-4" />
Finish Configuration
</Button>
) : (
<Button
onClick={() => setCurrentStep(Math.min(totalSteps, currentStep + 1))}
disabled={currentStep === totalSteps || !steps[currentStep].completed}
>
Next
<ChevronRight className="ml-2 h-4 w-4" />
</Button>
)}
</div>
</CardContent>
</Card>
</div>
);
};
+5 -5
View File
@@ -8,7 +8,7 @@ import PoolDashboard from '@/components/PoolDashboard';
import { CreatePoolModal } from './CreatePoolModal';
import { InitializeHezPoolModal } from './InitializeHezPoolModal';
import { InitializeUsdtModal } from './InitializeUsdtModal';
import { XCMBridgeSetupModal } from './XCMBridgeSetupModal';
import { XCMConfigurationWizard } from '@/components/admin/XCMConfigurationWizard';
import { ArrowRightLeft, Droplet, Settings } from 'lucide-react';
import { isFounderWallet } from '@pezkuwi/utils/auth';
@@ -138,15 +138,15 @@ export const DEXDashboard: React.FC = () => {
</div>
<div className="p-6 bg-gray-900 border border-purple-900/30 rounded-lg">
<h3 className="text-xl font-bold text-white mb-2">XCM Bridge Setup</h3>
<h3 className="text-xl font-bold text-white mb-2">XCM Configuration Wizard</h3>
<p className="text-gray-400 mb-6">
Configure Asset Hub USDT wUSDT bridge with one click. Enables cross-chain USDT transfers from Westend Asset Hub.
Complete 6-step parachain setup: Reserve ParaId, generate artifacts, register parachain, open HRMP channels, register foreign assets, and test XCM transfers.
</p>
<button
onClick={() => setShowXcmBridgeModal(true)}
className="w-full px-6 py-3 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition-colors font-medium"
>
Configure XCM Bridge
Open XCM Configuration Wizard
</button>
</div>
@@ -195,7 +195,7 @@ export const DEXDashboard: React.FC = () => {
onSuccess={handleSuccess}
/>
<XCMBridgeSetupModal
<XCMConfigurationWizard
isOpen={showXcmBridgeModal}
onClose={handleModalClose}
onSuccess={handleSuccess}
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
// Force reload for mock XCM update
import React, { useState, useEffect, useCallback } from 'react';
import { usePolkadot } from '@/contexts/PolkadotContext';
import { useWallet } from '@/contexts/WalletContext';
import { X, AlertCircle, Loader2, CheckCircle, Info, ExternalLink, Zap } from 'lucide-react';