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:
2025-10-27 23:52:36 +03:00
parent e5e359af52
commit 8482663c1b
8 changed files with 1734 additions and 379 deletions
+1137 -348
View File
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -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"
}
}
}
+12 -12
View File
@@ -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;
+14 -8
View File
@@ -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;
+164
View File
@@ -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>
);
};
+244
View File
@@ -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>
</>
);
};
+144
View File
@@ -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
View File
@@ -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>
);