mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 02:07:55 +00:00
feat: Integrate Polkadot.js API and wallet connection
Major Changes: - Added @polkadot/api and related dependencies - Created PolkadotContext for blockchain connection - Implemented NetworkStats component with live data - Added PolkadotWalletButton for wallet integration - Connected to local validator node (ws://127.0.0.1:9944) Features: - Live block number and hash display - Real-time validator count - Network connection status monitoring - Polkadot.js extension integration - Multi-account support - Account switching capability Technical: - RPC endpoint: ws://127.0.0.1:9944 - Auto-reconnect on disconnect - TypeScript integration - React hooks for state management
This commit is contained in:
Generated
+1137
-348
File diff suppressed because it is too large
Load Diff
+10
-5
@@ -12,6 +12,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@polkadot/api": "^16.4.9",
|
||||
"@polkadot/extension-dapp": "^0.62.3",
|
||||
"@polkadot/keyring": "^13.5.7",
|
||||
"@polkadot/util": "^13.5.7",
|
||||
"@polkadot/util-crypto": "^13.5.7",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||
@@ -48,6 +53,8 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
"i18next": "^23.7.6",
|
||||
"i18next-browser-languagedetector": "^7.2.0",
|
||||
"input-otp": "^1.2.4",
|
||||
"lucide-react": "^0.462.0",
|
||||
"marked": "^12.0.1",
|
||||
@@ -56,6 +63,7 @@
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-i18next": "^14.0.0",
|
||||
"react-resizable-panels": "^2.1.3",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"recharts": "^2.12.7",
|
||||
@@ -64,10 +72,7 @@
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^11.1.0",
|
||||
"vaul": "^0.9.3",
|
||||
"zod": "^3.23.8",
|
||||
"i18next": "^23.7.6",
|
||||
"react-i18next": "^14.0.0",
|
||||
"i18next-browser-languagedetector": "^7.2.0"
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
@@ -87,4 +92,4 @@
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import { P2PMarket } from './p2p/P2PMarket';
|
||||
import { MultiSigWallet } from './wallet/MultiSigWallet';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
|
||||
import { PolkadotWalletButton } from './PolkadotWalletButton';
|
||||
const AppLayout: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const [walletModalOpen, setWalletModalOpen] = useState(false);
|
||||
@@ -71,7 +71,7 @@ const AppLayout: React.FC = () => {
|
||||
<div className="min-h-screen bg-gray-950 text-white">
|
||||
{/* Navigation */}
|
||||
<nav className="fixed top-0 w-full z-40 bg-gray-950/90 backdrop-blur-md border-b border-gray-800">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="container mx-auto px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
<div className="flex items-center">
|
||||
<span className="text-xl font-bold bg-gradient-to-r from-green-500 to-yellow-400 bg-clip-text text-transparent">
|
||||
@@ -229,7 +229,7 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
<NotificationBell />
|
||||
<LanguageSwitcher />
|
||||
<WalletButton />
|
||||
<PolkadotWalletButton />
|
||||
<a
|
||||
href="https://github.com/pezkuwichain"
|
||||
target="_blank"
|
||||
@@ -258,19 +258,19 @@ const AppLayout: React.FC = () => {
|
||||
<DelegationManager />
|
||||
) : showForum ? (
|
||||
<div className="pt-20 min-h-screen bg-gray-950">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-full mx-auto px-8">
|
||||
<ForumOverview />
|
||||
</div>
|
||||
</div>
|
||||
) : showModeration ? (
|
||||
<div className="pt-20 min-h-screen bg-gray-950">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-full mx-auto px-8">
|
||||
<ModerationPanel />
|
||||
</div>
|
||||
</div>
|
||||
) : showTreasury ? (
|
||||
<div className="pt-20 min-h-screen bg-gray-950">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-full mx-auto px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||
{t('treasury.title', 'Treasury Management')}
|
||||
@@ -320,7 +320,7 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
) : showStaking ? (
|
||||
<div className="pt-20 min-h-screen bg-gray-950">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-full mx-auto px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||
Staking Rewards
|
||||
@@ -334,7 +334,7 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
) : showP2P ? (
|
||||
<div className="pt-20 min-h-screen bg-gray-950">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-full mx-auto px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||
P2P Trading Market
|
||||
@@ -348,7 +348,7 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
) : showTokenSwap ? (
|
||||
<div className="pt-20 min-h-screen bg-gray-950">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-full mx-auto px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-purple-500 via-pink-400 to-yellow-500 bg-clip-text text-transparent">
|
||||
PEZ/HEZ Token Swap
|
||||
@@ -362,7 +362,7 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
) : showMultiSig ? (
|
||||
<div className="pt-20 min-h-screen bg-gray-950">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-full mx-auto px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-4xl font-bold mb-4 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||
Multi-Signature Wallet
|
||||
@@ -424,7 +424,7 @@ const AppLayout: React.FC = () => {
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-gray-950 border-t border-gray-800 py-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="max-w-full mx-auto px-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4 bg-gradient-to-r from-green-500 to-yellow-400 bg-clip-text text-transparent">
|
||||
@@ -501,4 +501,4 @@ const AppLayout: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLayout;
|
||||
export default AppLayout;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChevronRight, Cpu, GitBranch, Shield } from 'lucide-react';
|
||||
import { NetworkStats } from './NetworkStats';
|
||||
|
||||
const HeroSection: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
return (
|
||||
<section className="relative min-h-screen flex items-center justify-center overflow-hidden bg-gray-950">
|
||||
<section className="relative min-h-screen flex items-center justify-start overflow-hidden bg-gray-950">
|
||||
{/* Kurdish Flag Background */}
|
||||
<div className="absolute inset-0">
|
||||
<img
|
||||
<img
|
||||
src="https://d64gsuwffb70l.cloudfront.net/68ec477a0a2fa844d6f9df15_1760373625599_6626c9cb.webp"
|
||||
alt="Kurdish Flag"
|
||||
className="w-full h-full object-cover opacity-30"
|
||||
@@ -18,7 +19,7 @@ const HeroSection: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<div className="relative z-10 w-full text-center">
|
||||
<div className="mb-8 inline-flex items-center px-4 py-2 rounded-full bg-green-600/20 backdrop-blur-sm border border-green-500/30">
|
||||
<Shield className="w-4 h-4 text-yellow-400 mr-2" />
|
||||
<span className="text-yellow-400 text-sm font-medium">Substrate Parachain v1.0</span>
|
||||
@@ -27,7 +28,7 @@ const HeroSection: React.FC = () => {
|
||||
<h1 className="text-5xl md:text-7xl font-bold mb-6 bg-gradient-to-r from-green-500 via-yellow-400 to-red-500 bg-clip-text text-transparent">
|
||||
PezkuwiChain
|
||||
</h1>
|
||||
|
||||
|
||||
<p className="text-xl md:text-2xl text-gray-300 mb-8 max-w-3xl mx-auto">
|
||||
{t('hero.title')}
|
||||
</p>
|
||||
@@ -35,6 +36,11 @@ const HeroSection: React.FC = () => {
|
||||
{t('hero.subtitle')}
|
||||
</p>
|
||||
|
||||
{/* Live Network Stats */}
|
||||
<div className="mb-12">
|
||||
<NetworkStats />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-12 max-w-4xl mx-auto">
|
||||
<div className="bg-gray-900/50 backdrop-blur-sm rounded-lg border border-green-500/30 p-4">
|
||||
<div className="text-2xl font-bold text-green-400">127</div>
|
||||
@@ -55,14 +61,14 @@ const HeroSection: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<button
|
||||
<button
|
||||
onClick={() => document.getElementById('governance')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
className="px-8 py-4 bg-gradient-to-r from-green-600 to-yellow-400 text-white font-semibold rounded-lg hover:from-green-700 hover:to-yellow-500 transition-all transform hover:scale-105 flex items-center justify-center group"
|
||||
>
|
||||
{t('hero.exploreGovernance')}
|
||||
<ChevronRight className="ml-2 w-5 h-5 group-hover:translate-x-1 transition-transform" />
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
onClick={() => document.getElementById('identity')?.scrollIntoView({ behavior: 'smooth' })}
|
||||
className="px-8 py-4 bg-gray-900/50 backdrop-blur-sm text-white font-semibold rounded-lg border border-red-500/50 hover:bg-red-500/10 transition-all"
|
||||
>
|
||||
@@ -74,4 +80,4 @@ const HeroSection: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroSection;
|
||||
export default HeroSection;
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Activity, Wifi, WifiOff, Users, Box, TrendingUp } from 'lucide-react';
|
||||
|
||||
export const NetworkStats: React.FC = () => {
|
||||
const { api, isApiReady, error } = usePolkadot();
|
||||
const [blockNumber, setBlockNumber] = useState<number>(0);
|
||||
const [blockHash, setBlockHash] = useState<string>('');
|
||||
const [finalizedBlock, setFinalizedBlock] = useState<number>(0);
|
||||
const [validatorCount, setValidatorCount] = useState<number>(0);
|
||||
const [peers, setPeers] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api || !isApiReady) return;
|
||||
|
||||
let unsubscribeNewHeads: () => void;
|
||||
let unsubscribeFinalizedHeads: () => void;
|
||||
|
||||
const subscribeToBlocks = async () => {
|
||||
try {
|
||||
// Subscribe to new blocks
|
||||
unsubscribeNewHeads = await api.rpc.chain.subscribeNewHeads((header) => {
|
||||
setBlockNumber(header.number.toNumber());
|
||||
setBlockHash(header.hash.toHex());
|
||||
});
|
||||
|
||||
// Subscribe to finalized blocks
|
||||
unsubscribeFinalizedHeads = await api.rpc.chain.subscribeFinalizedHeads((header) => {
|
||||
setFinalizedBlock(header.number.toNumber());
|
||||
});
|
||||
|
||||
// Get validator count
|
||||
const validators = await api.query.session.validators();
|
||||
setValidatorCount(validators.length);
|
||||
|
||||
// Get peer count
|
||||
const health = await api.rpc.system.health();
|
||||
setPeers(health.peers.toNumber());
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to subscribe to blocks:', err);
|
||||
}
|
||||
};
|
||||
|
||||
subscribeToBlocks();
|
||||
|
||||
return () => {
|
||||
if (unsubscribeNewHeads) unsubscribeNewHeads();
|
||||
if (unsubscribeFinalizedHeads) unsubscribeFinalizedHeads();
|
||||
};
|
||||
}, [api, isApiReady]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Card className="bg-red-950/50 border-red-900">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-red-400">
|
||||
<WifiOff className="w-5 h-5" />
|
||||
Network Disconnected
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-red-300 text-sm">{error}</p>
|
||||
<p className="text-red-400 text-xs mt-2">
|
||||
Make sure your validator node is running at ws://127.0.0.1:9944
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isApiReady) {
|
||||
return (
|
||||
<Card className="bg-gray-900 border-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Activity className="w-5 h-5 animate-pulse" />
|
||||
Connecting to Network...
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{/* Connection Status */}
|
||||
<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">
|
||||
<Wifi className="w-4 h-4 text-green-500" />
|
||||
Network Status
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge className="bg-green-500/20 text-green-400 border-green-500/50">
|
||||
Connected
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">{peers} peers</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Latest Block */}
|
||||
<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">
|
||||
<Box className="w-4 h-4 text-blue-500" />
|
||||
Latest Block
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-1">
|
||||
<div className="text-2xl font-bold text-white">
|
||||
#{blockNumber.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 font-mono truncate">
|
||||
{blockHash.slice(0, 10)}...{blockHash.slice(-8)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Finalized Block */}
|
||||
<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">
|
||||
<TrendingUp className="w-4 h-4 text-purple-500" />
|
||||
Finalized Block
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
#{finalizedBlock.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
{blockNumber - finalizedBlock} blocks behind
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Validators */}
|
||||
<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-yellow-500" />
|
||||
Active Validators
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">
|
||||
{validatorCount}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
Securing the network
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,244 @@
|
||||
import React, { useState } from 'react';
|
||||
import { usePolkadot } from '@/contexts/PolkadotContext';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Wallet, Check, ExternalLink, Copy, LogOut } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
export const PolkadotWalletButton: React.FC = () => {
|
||||
const {
|
||||
accounts,
|
||||
selectedAccount,
|
||||
setSelectedAccount,
|
||||
connectWallet,
|
||||
disconnectWallet,
|
||||
error
|
||||
} = usePolkadot();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [balance, setBalance] = useState<string>('0');
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleConnect = async () => {
|
||||
await connectWallet();
|
||||
if (accounts.length > 0) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectAccount = (account: typeof accounts[0]) => {
|
||||
setSelectedAccount(account);
|
||||
setIsOpen(false);
|
||||
toast({
|
||||
title: "Account Connected",
|
||||
description: `${account.meta.name} - ${formatAddress(account.address)}`,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
disconnectWallet();
|
||||
toast({
|
||||
title: "Wallet Disconnected",
|
||||
description: "Your wallet has been disconnected",
|
||||
});
|
||||
};
|
||||
|
||||
const formatAddress = (address: string) => {
|
||||
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||
};
|
||||
|
||||
const copyAddress = () => {
|
||||
if (selectedAccount) {
|
||||
navigator.clipboard.writeText(selectedAccount.address);
|
||||
toast({
|
||||
title: "Address Copied",
|
||||
description: "Address copied to clipboard",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedAccount) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="bg-green-500/20 border-green-500/50 text-green-400 hover:bg-green-500/30"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Wallet className="w-4 h-4 mr-2" />
|
||||
{selectedAccount.meta.name || 'Account'}
|
||||
<Badge className="ml-2 bg-green-500/30 text-green-300 border-0">
|
||||
{formatAddress(selectedAccount.address)}
|
||||
</Badge>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleDisconnect}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Account Details</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Your connected Polkadot account
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-400 mb-1">Account Name</div>
|
||||
<div className="text-white font-medium">
|
||||
{selectedAccount.meta.name || 'Unnamed Account'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-400 mb-1">Address</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<code className="text-white text-sm font-mono">
|
||||
{selectedAccount.address}
|
||||
</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={copyAddress}
|
||||
className="text-gray-400 hover:text-white"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-800/50 rounded-lg p-4">
|
||||
<div className="text-sm text-gray-400 mb-1">Source</div>
|
||||
<div className="text-white">
|
||||
{selectedAccount.meta.source || 'polkadot-js'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{accounts.length > 1 && (
|
||||
<div>
|
||||
<div className="text-sm text-gray-400 mb-2">Switch Account</div>
|
||||
<div className="space-y-2">
|
||||
{accounts.map((account) => (
|
||||
<button
|
||||
key={account.address}
|
||||
onClick={() => handleSelectAccount(account)}
|
||||
className={`w-full p-3 rounded-lg border transition-all flex items-center justify-between ${
|
||||
account.address === selectedAccount.address
|
||||
? 'bg-green-500/20 border-green-500/50'
|
||||
: 'bg-gray-800/50 border-gray-700 hover:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<div className="text-left">
|
||||
<div className="text-white font-medium">
|
||||
{account.meta.name || 'Unnamed'}
|
||||
</div>
|
||||
<div className="text-gray-400 text-xs font-mono">
|
||||
{formatAddress(account.address)}
|
||||
</div>
|
||||
</div>
|
||||
{account.address === selectedAccount.address && (
|
||||
<Check className="w-5 h-5 text-green-400" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleConnect}
|
||||
className="bg-gradient-to-r from-green-600 to-yellow-400 hover:from-green-700 hover:to-yellow-500 text-white"
|
||||
>
|
||||
<Wallet className="w-4 h-4 mr-2" />
|
||||
Connect Wallet
|
||||
</Button>
|
||||
|
||||
{error && error.includes('install Polkadot.js') && (
|
||||
<Dialog open={!!error} onOpenChange={() => {}}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Install Polkadot.js Extension</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
You need the Polkadot.js browser extension to connect your wallet
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-300">
|
||||
The Polkadot.js extension allows you to manage your accounts and sign transactions securely.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<a
|
||||
href="https://polkadot.js.org/extension/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex-1"
|
||||
>
|
||||
<Button className="w-full bg-orange-600 hover:bg-orange-700">
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
Install Extension
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-gray-500">
|
||||
After installing, refresh this page and click "Connect Wallet" again.
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
<Dialog open={isOpen && accounts.length > 0} onOpenChange={setIsOpen}>
|
||||
<DialogContent className="bg-gray-900 border-gray-800">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-white">Select Account</DialogTitle>
|
||||
<DialogDescription className="text-gray-400">
|
||||
Choose an account to connect
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-2">
|
||||
{accounts.map((account) => (
|
||||
<button
|
||||
key={account.address}
|
||||
onClick={() => handleSelectAccount(account)}
|
||||
className="w-full p-4 rounded-lg border border-gray-700 bg-gray-800/50 hover:border-green-500/50 hover:bg-gray-800 transition-all text-left"
|
||||
>
|
||||
<div className="text-white font-medium mb-1">
|
||||
{account.meta.name || 'Unnamed Account'}
|
||||
</div>
|
||||
<div className="text-gray-400 text-sm font-mono">
|
||||
{account.address}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import { ApiPromise, WsProvider } from '@polkadot/api';
|
||||
import { web3Accounts, web3Enable, web3FromAddress } from '@polkadot/extension-dapp';
|
||||
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
|
||||
|
||||
interface PolkadotContextType {
|
||||
api: ApiPromise | null;
|
||||
isApiReady: boolean;
|
||||
accounts: InjectedAccountWithMeta[];
|
||||
selectedAccount: InjectedAccountWithMeta | null;
|
||||
setSelectedAccount: (account: InjectedAccountWithMeta | null) => void;
|
||||
connectWallet: () => Promise<void>;
|
||||
disconnectWallet: () => void;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const PolkadotContext = createContext<PolkadotContextType | undefined>(undefined);
|
||||
|
||||
interface PolkadotProviderProps {
|
||||
children: ReactNode;
|
||||
endpoint?: string;
|
||||
}
|
||||
|
||||
export const PolkadotProvider: React.FC<PolkadotProviderProps> = ({
|
||||
children,
|
||||
endpoint = 'ws://127.0.0.1:9944' // Local testnet RPC
|
||||
}) => {
|
||||
const [api, setApi] = useState<ApiPromise | null>(null);
|
||||
const [isApiReady, setIsApiReady] = useState(false);
|
||||
const [accounts, setAccounts] = useState<InjectedAccountWithMeta[]>([]);
|
||||
const [selectedAccount, setSelectedAccount] = useState<InjectedAccountWithMeta | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Initialize Polkadot API
|
||||
useEffect(() => {
|
||||
const initApi = async () => {
|
||||
try {
|
||||
console.log('🔗 Connecting to Pezkuwi node:', endpoint);
|
||||
|
||||
const provider = new WsProvider(endpoint);
|
||||
const apiInstance = await ApiPromise.create({ provider });
|
||||
|
||||
await apiInstance.isReady;
|
||||
|
||||
setApi(apiInstance);
|
||||
setIsApiReady(true);
|
||||
setError(null);
|
||||
|
||||
console.log('✅ Connected to Pezkuwi node');
|
||||
|
||||
// Get chain info
|
||||
const [chain, nodeName, nodeVersion] = await Promise.all([
|
||||
apiInstance.rpc.system.chain(),
|
||||
apiInstance.rpc.system.name(),
|
||||
apiInstance.rpc.system.version(),
|
||||
]);
|
||||
|
||||
console.log(`📡 Chain: ${chain}`);
|
||||
console.log(`🖥️ Node: ${nodeName} v${nodeVersion}`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Failed to connect to node:', err);
|
||||
setError(`Failed to connect to node: ${endpoint}`);
|
||||
setIsApiReady(false);
|
||||
}
|
||||
};
|
||||
|
||||
initApi();
|
||||
|
||||
return () => {
|
||||
if (api) {
|
||||
api.disconnect();
|
||||
}
|
||||
};
|
||||
}, [endpoint]);
|
||||
|
||||
// Connect wallet (Polkadot.js extension)
|
||||
const connectWallet = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
// Enable extension
|
||||
const extensions = await web3Enable('PezkuwiChain');
|
||||
|
||||
if (extensions.length === 0) {
|
||||
setError('Please install Polkadot.js extension');
|
||||
window.open('https://polkadot.js.org/extension/', '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('✅ Polkadot.js extension enabled');
|
||||
|
||||
// Get accounts
|
||||
const allAccounts = await web3Accounts();
|
||||
|
||||
if (allAccounts.length === 0) {
|
||||
setError('No accounts found. Please create an account in Polkadot.js extension');
|
||||
return;
|
||||
}
|
||||
|
||||
setAccounts(allAccounts);
|
||||
setSelectedAccount(allAccounts[0]); // Auto-select first account
|
||||
|
||||
console.log(`✅ Found ${allAccounts.length} account(s)`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('❌ Wallet connection failed:', err);
|
||||
setError('Failed to connect wallet');
|
||||
}
|
||||
};
|
||||
|
||||
// Disconnect wallet
|
||||
const disconnectWallet = () => {
|
||||
setAccounts([]);
|
||||
setSelectedAccount(null);
|
||||
console.log('🔌 Wallet disconnected');
|
||||
};
|
||||
|
||||
const value: PolkadotContextType = {
|
||||
api,
|
||||
isApiReady,
|
||||
accounts,
|
||||
selectedAccount,
|
||||
setSelectedAccount,
|
||||
connectWallet,
|
||||
disconnectWallet,
|
||||
error,
|
||||
};
|
||||
|
||||
return (
|
||||
<PolkadotContext.Provider value={value}>
|
||||
{children}
|
||||
</PolkadotContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// Hook to use Polkadot context
|
||||
export const usePolkadot = (): PolkadotContextType => {
|
||||
const context = useContext(PolkadotContext);
|
||||
if (!context) {
|
||||
throw new Error('usePolkadot must be used within PolkadotProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
+9
-6
@@ -2,6 +2,7 @@ import { createRoot } from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import { WalletProvider } from './contexts/WalletContext'
|
||||
import { WebSocketProvider } from './contexts/WebSocketContext'
|
||||
import { PolkadotProvider } from './contexts/PolkadotContext'
|
||||
import './index.css'
|
||||
import './i18n/config'
|
||||
|
||||
@@ -14,9 +15,11 @@ declare global {
|
||||
|
||||
// Remove dark mode class addition
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<WalletProvider>
|
||||
<WebSocketProvider>
|
||||
<App />
|
||||
</WebSocketProvider>
|
||||
</WalletProvider>
|
||||
);
|
||||
<PolkadotProvider endpoint="ws://127.0.0.1:9944">
|
||||
<WalletProvider>
|
||||
<WebSocketProvider>
|
||||
<App />
|
||||
</WebSocketProvider>
|
||||
</WalletProvider>
|
||||
</PolkadotProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user