Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "papi-simulator",
"version": "0.4.2",
"author": "codingsh <codinsh@pm.me>",
"author": "codingsh <codingsh@pm.me>",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
Expand Down
89 changes: 36 additions & 53 deletions src/components/blockchain/BlockchainDashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
import dynamic from "next/dynamic";
import { networkManager, type Network, NetworkSchema } from "./dynamic-blockchain-config";


const WalletAccountSchema = z.object({
address: z.string(),
meta: z.object({
Expand Down Expand Up @@ -61,7 +60,6 @@ const BlockExplorer = createDynamicComponent(() => import("./components/BlockExp
const TransactionMonitor = createDynamicComponent(() => import("./components/TransactionMonitor"), "Transaction Monitor");
const TransactionBuilder = createDynamicComponent(() => import("./components/TransactionBuilder"), "Transaction Builder");

// Enhanced Polkadot API hook
const usePolkadotApi = (network: Network, isWalletConnected: boolean): ChainConnectionState => {
const [state, setState] = useState<ChainConnectionState>({
api: null,
Expand Down Expand Up @@ -92,7 +90,6 @@ const usePolkadotApi = (network: Network, isWalletConnected: boolean): ChainConn
const connectionKey = `${network.rpcUrl}-${isWalletConnected}`;
if (connectionAttemptRef.current === connectionKey) return;

// Abort previous connection
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
Expand All @@ -111,18 +108,24 @@ const usePolkadotApi = (network: Network, isWalletConnected: boolean): ChainConn
setState(prev => ({ ...prev, status: "connecting", error: null }));

provider = new WsProvider(network.rpcUrl, 1000);

provider.on('disconnected', () => {
const handleDisconnect = () => {
if (isMounted && mountedRef.current) {
setState(prev => ({ ...prev, status: "disconnected" }));
}
});
};

provider.on('error', (error) => {
const handleError = (error: any) => {
if (isMounted && mountedRef.current) {
setState(prev => ({ ...prev, status: "error", error: error.message }));
}
});
};


if (typeof provider.on === 'function') {
provider.on('disconnected', handleDisconnect);
provider.on('error', handleError);
}

apiInstance = await Promise.race([
ApiPromise.create({
Expand Down Expand Up @@ -163,11 +166,16 @@ const usePolkadotApi = (network: Network, isWalletConnected: boolean): ChainConn

const cleanup = async () => {
try {

if (provider) {
provider.removeAllListeners();
await provider.disconnect();
if (typeof provider.removeAllListeners === 'function') {
provider.removeAllListeners();
}
if (typeof provider.disconnect === 'function') {
await provider.disconnect();
}
}
if (apiInstance) {
if (apiInstance && typeof apiInstance.disconnect === 'function') {
await apiInstance.disconnect();
}
} catch (error) {
Expand All @@ -182,7 +190,6 @@ const usePolkadotApi = (network: Network, isWalletConnected: boolean): ChainConn
return state;
};

// Utility functions
const getAccountDisplayName = (account: WalletAccount | null): string =>
account?.meta?.name ||
account?.name ||
Expand Down Expand Up @@ -212,40 +219,27 @@ const getNetworkTypeIcon = (type: Network["type"], className = "w-4 h-4") => {
return icons[type] || <Globe className={className} aria-hidden="true" />;
};

// UI Components
const StatusIndicator = memo<{ status: ConnectionStatus; showLabel?: boolean }>(({ status, showLabel = true }) => {
const colors = {
connected: "bg-green-500",
connecting: "bg-yellow-500 animate-pulse",
disconnected: "bg-gray-400",
error: "bg-red-500",
};

const labels = {
connected: "Connected",
connecting: "Connecting",
disconnected: "Disconnected",
error: "Error",
const config = {
connected: { color: "bg-green-500", label: "Connected", icon: <Wifi className="w-3 h-3" /> },
connecting: { color: "bg-yellow-500 animate-pulse", label: "Connecting", icon: <div className="w-3 h-3 animate-spin rounded-full border border-gray-300 border-t-white" /> },
disconnected: { color: "bg-gray-400", label: "Disconnected", icon: <WifiOff className="w-3 h-3" /> },
error: { color: "bg-red-500", label: "Error", icon: <AlertCircle className="w-3 h-3" /> },
};

const icons = {
connected: <Wifi className="w-3 h-3" aria-hidden="true" />,
connecting: <div className="w-3 h-3 animate-spin rounded-full border border-gray-300 border-t-white" />,
disconnected: <WifiOff className="w-3 h-3" aria-hidden="true" />,
error: <AlertCircle className="w-3 h-3" aria-hidden="true" />,
};
const { color, label, icon } = config[status];

return (
<div className="flex items-center space-x-2" role="status" aria-label={`${labels[status]} status`}>
<div className={`w-2 h-2 rounded-full ${colors[status]} relative overflow-hidden`}>
<div className="flex items-center space-x-2" role="status" aria-label={`${label} status`}>
<div className={`w-2 h-2 rounded-full ${color} relative overflow-hidden`}>
{status === "connecting" && (
<div className="absolute inset-0 bg-yellow-500 animate-ping" />
)}
</div>
{showLabel && (
<div className="flex items-center space-x-1">
<span className="text-xs text-theme-secondary">{labels[status]}</span>
<span className="text-theme-tertiary">{icons[status]}</span>
<span className="text-xs text-theme-secondary">{label}</span>
<span className="text-theme-tertiary">{icon}</span>
</div>
)}
</div>
Expand Down Expand Up @@ -315,25 +309,20 @@ const NetworkSelector = memo<{

const networks = useMemo(() => {
let filtered = networkManager.getAllNetworks();

if (selectedFilter !== "all") {
filtered = networkManager.getNetworksByType(selectedFilter);
}

if (searchQuery) {
filtered = networkManager.searchNetworks(searchQuery);
}

return filtered;
}, [searchQuery, selectedFilter]);

const networkGroups = useMemo(() => {
if (selectedFilter !== "all") {
return { [selectedFilter]: networks };
}

const groups = networkManager.getNetworkGroups();

if (searchQuery) {
Object.keys(groups).forEach(key => {
groups[key] = groups[key].filter(network =>
Expand All @@ -344,7 +333,6 @@ const NetworkSelector = memo<{
}
});
}

return groups;
}, [networks, selectedFilter, searchQuery]);

Expand Down Expand Up @@ -629,12 +617,13 @@ const ConnectionStatusDisplay = memo<{
if (layoutMode === "transaction-fullscreen") return null;

const getStatusColor = (status: string) => {
switch (status) {
case "connected": return "border-green-200/70 dark:border-green-600/70 bg-green-50/80 dark:bg-green-900/40";
case "connecting": return "border-yellow-200/70 dark:border-yellow-600/70 bg-yellow-50/80 dark:bg-yellow-900/40";
case "error": return "border-red-200/70 dark:border-red-600/70 bg-red-50/80 dark:bg-red-900/40";
default: return "border-theme bg-theme-surface";
}
const colors = {
connected: "border-green-200/70 dark:border-green-600/70 bg-green-50/80 dark:bg-green-900/40",
connecting: "border-yellow-200/70 dark:border-yellow-600/70 bg-yellow-50/80 dark:bg-yellow-900/40",
error: "border-red-200/70 dark:border-red-600/70 bg-red-50/80 dark:bg-red-900/40",
default: "border-theme bg-theme-surface",
};
return colors[status] || colors.default;
};

return (
Expand Down Expand Up @@ -889,12 +878,6 @@ const KeyboardShortcutsHelp = memo<{ layoutMode: LayoutMode; selectedNetwork: Ne
Esc
</kbd>
</div>
<div className="flex items-center justify-between">
<span className="text-theme-secondary">Search Networks:</span>
<kbd className="bg-theme-surface-variant px-2 py-1 rounded border text-theme-primary font-mono">
{modifierKey}+K
</kbd>
</div>
</div>
<button
onClick={() => setIsVisible(false)}
Expand Down
23 changes: 1 addition & 22 deletions src/components/blockchain/EventMonitor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Badge, { type BadgeVariant } from "@/components/ui/Badge";
import { Button } from "@/components/ui";
import { Activity, Pause, Play, Trash2, Eye, EyeOff, Filter, AlertCircle } from "lucide-react";
import type { ApiPromise } from "@polkadot/api";

import { buildSubscanUrl } from "@/lib/utils/explorer";

interface ChainEvent {
id: string;
Expand Down Expand Up @@ -39,28 +39,7 @@ interface EventMonitorProps {
}


const buildSubscanUrl = (network: Network, type: 'block' | 'extrinsic' | 'account', identifier: string | number): string => {
const baseUrl = network.explorer?.replace(/\/$/, '') || '';


if (!baseUrl || baseUrl === 'undefined' || !baseUrl.includes('subscan.io')) {
console.warn('Invalid explorer URL:', network.explorer);
const networkName = network.name?.toLowerCase() || 'polkadot';
const fallbackUrl = `https://${networkName}.subscan.io`;
return `${fallbackUrl}/${type}/${identifier}`;
}

switch (type) {
case 'block':
return `${baseUrl}/block/${identifier}`;
case 'extrinsic':
return `${baseUrl}/extrinsic/${identifier}`;
case 'account':
return `${baseUrl}/account/${identifier}`;
default:
return baseUrl;
}
};


const useEventMonitor = (api: ApiPromise, limit: number) => {
Expand Down
Loading