mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-06-13 11:11:01 +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
+9
-4
@@ -12,6 +12,11 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@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-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||||
@@ -48,6 +53,8 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.3.0",
|
"embla-carousel-react": "^8.3.0",
|
||||||
"highlight.js": "^11.9.0",
|
"highlight.js": "^11.9.0",
|
||||||
|
"i18next": "^23.7.6",
|
||||||
|
"i18next-browser-languagedetector": "^7.2.0",
|
||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"marked": "^12.0.1",
|
"marked": "^12.0.1",
|
||||||
@@ -56,6 +63,7 @@
|
|||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-hook-form": "^7.53.0",
|
"react-hook-form": "^7.53.0",
|
||||||
|
"react-i18next": "^14.0.0",
|
||||||
"react-resizable-panels": "^2.1.3",
|
"react-resizable-panels": "^2.1.3",
|
||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
@@ -64,10 +72,7 @@
|
|||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "^0.9.3",
|
"vaul": "^0.9.3",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8"
|
||||||
"i18next": "^23.7.6",
|
|
||||||
"react-i18next": "^14.0.0",
|
|
||||||
"i18next-browser-languagedetector": "^7.2.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.0",
|
"@eslint/js": "^9.9.0",
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import { P2PMarket } from './p2p/P2PMarket';
|
|||||||
import { MultiSigWallet } from './wallet/MultiSigWallet';
|
import { MultiSigWallet } from './wallet/MultiSigWallet';
|
||||||
import { useWallet } from '@/contexts/WalletContext';
|
import { useWallet } from '@/contexts/WalletContext';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
|
import { PolkadotWalletButton } from './PolkadotWalletButton';
|
||||||
const AppLayout: React.FC = () => {
|
const AppLayout: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [walletModalOpen, setWalletModalOpen] = useState(false);
|
const [walletModalOpen, setWalletModalOpen] = useState(false);
|
||||||
@@ -71,7 +71,7 @@ const AppLayout: React.FC = () => {
|
|||||||
<div className="min-h-screen bg-gray-950 text-white">
|
<div className="min-h-screen bg-gray-950 text-white">
|
||||||
{/* Navigation */}
|
{/* Navigation */}
|
||||||
<nav className="fixed top-0 w-full z-40 bg-gray-950/90 backdrop-blur-md border-b border-gray-800">
|
<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 justify-between h-16">
|
||||||
<div className="flex items-center">
|
<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">
|
<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>
|
</div>
|
||||||
<NotificationBell />
|
<NotificationBell />
|
||||||
<LanguageSwitcher />
|
<LanguageSwitcher />
|
||||||
<WalletButton />
|
<PolkadotWalletButton />
|
||||||
<a
|
<a
|
||||||
href="https://github.com/pezkuwichain"
|
href="https://github.com/pezkuwichain"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -258,19 +258,19 @@ const AppLayout: React.FC = () => {
|
|||||||
<DelegationManager />
|
<DelegationManager />
|
||||||
) : showForum ? (
|
) : showForum ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<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 />
|
<ForumOverview />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : showModeration ? (
|
) : showModeration ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<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 />
|
<ModerationPanel />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : showTreasury ? (
|
) : showTreasury ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<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">
|
<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">
|
<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')}
|
{t('treasury.title', 'Treasury Management')}
|
||||||
@@ -320,7 +320,7 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : showStaking ? (
|
) : showStaking ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<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">
|
<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">
|
<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
|
Staking Rewards
|
||||||
@@ -334,7 +334,7 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : showP2P ? (
|
) : showP2P ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<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">
|
<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">
|
<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
|
P2P Trading Market
|
||||||
@@ -348,7 +348,7 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : showTokenSwap ? (
|
) : showTokenSwap ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<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">
|
<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">
|
<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
|
PEZ/HEZ Token Swap
|
||||||
@@ -362,7 +362,7 @@ const AppLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : showMultiSig ? (
|
) : showMultiSig ? (
|
||||||
<div className="pt-20 min-h-screen bg-gray-950">
|
<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">
|
<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">
|
<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
|
Multi-Signature Wallet
|
||||||
@@ -424,7 +424,7 @@ const AppLayout: React.FC = () => {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="bg-gray-950 border-t border-gray-800 py-12">
|
<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 className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg font-semibold mb-4 bg-gradient-to-r from-green-500 to-yellow-400 bg-clip-text text-transparent">
|
<h3 className="text-lg font-semibold mb-4 bg-gradient-to-r from-green-500 to-yellow-400 bg-clip-text text-transparent">
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { ChevronRight, Cpu, GitBranch, Shield } from 'lucide-react';
|
import { ChevronRight, Cpu, GitBranch, Shield } from 'lucide-react';
|
||||||
|
import { NetworkStats } from './NetworkStats';
|
||||||
|
|
||||||
const HeroSection: React.FC = () => {
|
const HeroSection: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Kurdish Flag Background */}
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
<img
|
<img
|
||||||
@@ -18,7 +19,7 @@ const HeroSection: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* 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">
|
<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" />
|
<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>
|
<span className="text-yellow-400 text-sm font-medium">Substrate Parachain v1.0</span>
|
||||||
@@ -35,6 +36,11 @@ const HeroSection: React.FC = () => {
|
|||||||
{t('hero.subtitle')}
|
{t('hero.subtitle')}
|
||||||
</p>
|
</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="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="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>
|
<div className="text-2xl font-bold text-green-400">127</div>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
+8
-5
@@ -2,6 +2,7 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import { WalletProvider } from './contexts/WalletContext'
|
import { WalletProvider } from './contexts/WalletContext'
|
||||||
import { WebSocketProvider } from './contexts/WebSocketContext'
|
import { WebSocketProvider } from './contexts/WebSocketContext'
|
||||||
|
import { PolkadotProvider } from './contexts/PolkadotContext'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import './i18n/config'
|
import './i18n/config'
|
||||||
|
|
||||||
@@ -14,9 +15,11 @@ declare global {
|
|||||||
|
|
||||||
// Remove dark mode class addition
|
// Remove dark mode class addition
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<WalletProvider>
|
<PolkadotProvider endpoint="ws://127.0.0.1:9944">
|
||||||
<WebSocketProvider>
|
<WalletProvider>
|
||||||
<App />
|
<WebSocketProvider>
|
||||||
</WebSocketProvider>
|
<App />
|
||||||
</WalletProvider>
|
</WebSocketProvider>
|
||||||
|
</WalletProvider>
|
||||||
|
</PolkadotProvider>
|
||||||
);
|
);
|
||||||
Reference in New Issue
Block a user