mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 19:27:56 +00:00
Reorganize repository into monorepo structure
Restructured the project to support multiple frontend applications: - Move web app to web/ directory - Create pezkuwi-sdk-ui/ for Polkadot SDK clone (planned) - Create mobile/ directory for mobile app development - Add shared/ directory with common utilities, types, and blockchain code - Update README.md with comprehensive documentation - Remove obsolete DKSweb/ directory This monorepo structure enables better code sharing and organized development across web, mobile, and SDK UI projects.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
|
||||
interface AppContextType {
|
||||
sidebarOpen: boolean;
|
||||
toggleSidebar: () => void;
|
||||
}
|
||||
|
||||
const defaultAppContext: AppContextType = {
|
||||
sidebarOpen: false,
|
||||
toggleSidebar: () => {},
|
||||
};
|
||||
|
||||
const AppContext = createContext<AppContextType>(defaultAppContext);
|
||||
|
||||
export const useAppContext = () => useContext(AppContext);
|
||||
|
||||
export const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setSidebarOpen(prev => !prev);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
sidebarOpen,
|
||||
toggleSidebar,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AppContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,217 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { User } from '@supabase/supabase-js';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
isAdmin: boolean;
|
||||
signIn: (email: string, password: string) => Promise<{ error: any }>;
|
||||
signUp: (email: string, password: string, username: string, referralCode?: string) => Promise<{ error: any }>;
|
||||
signOut: () => Promise<void>;
|
||||
checkAdminStatus: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
// Demo/Founder account credentials from environment variables
|
||||
// ⚠️ SECURITY: Never hardcode credentials in source code!
|
||||
const FOUNDER_ACCOUNT = {
|
||||
email: import.meta.env.VITE_DEMO_FOUNDER_EMAIL || '',
|
||||
password: import.meta.env.VITE_DEMO_FOUNDER_PASSWORD || '',
|
||||
id: import.meta.env.VITE_DEMO_FOUNDER_ID || 'founder-001',
|
||||
user_metadata: {
|
||||
full_name: 'Satoshi Qazi Muhammed',
|
||||
phone: '+9647700557978',
|
||||
recovery_email: 'satoshi@pezkuwichain.io',
|
||||
founder: true
|
||||
}
|
||||
};
|
||||
|
||||
// Check if demo mode is enabled
|
||||
const DEMO_MODE_ENABLED = import.meta.env.VITE_ENABLE_DEMO_MODE === 'true';
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check active sessions and sets the user
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
setUser(session?.user ?? null);
|
||||
if (session?.user) {
|
||||
checkAdminStatus();
|
||||
}
|
||||
setLoading(false);
|
||||
}).catch(() => {
|
||||
// If Supabase is not available, continue without auth
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
// Listen for changes on auth state
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setUser(session?.user ?? null);
|
||||
if (session?.user) {
|
||||
checkAdminStatus();
|
||||
}
|
||||
setLoading(false);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
const checkAdminStatus = async () => {
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (!user) return false;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('admin_roles')
|
||||
.select('role')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
const adminStatus = !error && data && ['admin', 'super_admin'].includes(data.role);
|
||||
setIsAdmin(adminStatus);
|
||||
return adminStatus;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const signIn = async (email: string, password: string) => {
|
||||
// Check if demo mode is enabled and this is the founder account
|
||||
if (DEMO_MODE_ENABLED && email === FOUNDER_ACCOUNT.email && password === FOUNDER_ACCOUNT.password) {
|
||||
// Try Supabase first
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (!error && data.user) {
|
||||
await checkAdminStatus();
|
||||
return { error: null };
|
||||
}
|
||||
} catch {
|
||||
// Supabase not available
|
||||
}
|
||||
|
||||
// Fallback to demo mode for founder account
|
||||
const demoUser = {
|
||||
id: FOUNDER_ACCOUNT.id,
|
||||
email: FOUNDER_ACCOUNT.email,
|
||||
user_metadata: FOUNDER_ACCOUNT.user_metadata,
|
||||
email_confirmed_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
} as User;
|
||||
|
||||
setUser(demoUser);
|
||||
setIsAdmin(true);
|
||||
|
||||
// Store in localStorage for persistence
|
||||
localStorage.setItem('demo_user', JSON.stringify(demoUser));
|
||||
|
||||
return { error: null };
|
||||
}
|
||||
|
||||
// For other accounts, use Supabase
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (!error && data.user) {
|
||||
await checkAdminStatus();
|
||||
}
|
||||
|
||||
return { error };
|
||||
} catch (err) {
|
||||
return {
|
||||
error: {
|
||||
message: 'Authentication service unavailable. Please try again later.'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const signUp = async (email: string, password: string, username: string, referralCode?: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
data: {
|
||||
username,
|
||||
referral_code: referralCode || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!error && data.user) {
|
||||
// Create profile in profiles table with referral code
|
||||
await supabase.from('profiles').insert({
|
||||
id: data.user.id,
|
||||
username,
|
||||
email,
|
||||
referred_by: referralCode || null,
|
||||
});
|
||||
|
||||
// If there's a referral code, track it
|
||||
if (referralCode) {
|
||||
// You can add logic here to reward the referrer
|
||||
// For example, update their referral count or add rewards
|
||||
console.log(`User registered with referral code: ${referralCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { error };
|
||||
} catch (err) {
|
||||
return {
|
||||
error: {
|
||||
message: 'Registration service unavailable. Please try again later.'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const signOut = async () => {
|
||||
localStorage.removeItem('demo_user');
|
||||
setIsAdmin(false);
|
||||
await supabase.auth.signOut();
|
||||
};
|
||||
|
||||
// Check for demo user on mount
|
||||
useEffect(() => {
|
||||
const demoUser = localStorage.getItem('demo_user');
|
||||
if (demoUser && !user) {
|
||||
const parsedUser = JSON.parse(demoUser);
|
||||
setUser(parsedUser);
|
||||
setIsAdmin(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{
|
||||
user,
|
||||
loading,
|
||||
isAdmin,
|
||||
signIn,
|
||||
signUp,
|
||||
signOut,
|
||||
checkAdminStatus
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,161 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { useWallet } from './WalletContext';
|
||||
import {
|
||||
IdentityProfile,
|
||||
KYCData,
|
||||
Badge,
|
||||
Role,
|
||||
calculateReputationScore,
|
||||
generateZKProof,
|
||||
DEFAULT_BADGES,
|
||||
ROLES
|
||||
} from '@/lib/identity';
|
||||
|
||||
interface IdentityContextType {
|
||||
profile: IdentityProfile | null;
|
||||
isVerifying: boolean;
|
||||
startKYC: (data: KYCData) => Promise<void>;
|
||||
updatePrivacySettings: (settings: any) => void;
|
||||
addBadge: (badge: Badge) => void;
|
||||
assignRole: (role: Role) => void;
|
||||
refreshReputation: () => void;
|
||||
}
|
||||
|
||||
const IdentityContext = createContext<IdentityContextType | undefined>(undefined);
|
||||
|
||||
export function IdentityProvider({ children }: { children: React.ReactNode }) {
|
||||
const { account } = useWallet();
|
||||
const [profile, setProfile] = useState<IdentityProfile | null>(null);
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (account) {
|
||||
// Load or create profile for connected wallet
|
||||
const storedProfile = localStorage.getItem(`identity_${account}`);
|
||||
if (storedProfile) {
|
||||
setProfile(JSON.parse(storedProfile));
|
||||
} else {
|
||||
// Create new profile
|
||||
const newProfile: IdentityProfile = {
|
||||
address: account,
|
||||
verificationLevel: 'none',
|
||||
kycStatus: 'none',
|
||||
reputationScore: 0,
|
||||
badges: [],
|
||||
roles: [],
|
||||
privacySettings: {
|
||||
showRealName: false,
|
||||
showEmail: false,
|
||||
showCountry: true,
|
||||
useZKProof: true
|
||||
}
|
||||
};
|
||||
setProfile(newProfile);
|
||||
localStorage.setItem(`identity_${account}`, JSON.stringify(newProfile));
|
||||
}
|
||||
} else {
|
||||
setProfile(null);
|
||||
}
|
||||
}, [account]);
|
||||
|
||||
const startKYC = async (data: KYCData) => {
|
||||
if (!profile) return;
|
||||
|
||||
setIsVerifying(true);
|
||||
try {
|
||||
// Simulate KYC verification process
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
const zkProof = generateZKProof(data);
|
||||
|
||||
const updatedProfile: IdentityProfile = {
|
||||
...profile,
|
||||
kycStatus: 'approved',
|
||||
verificationLevel: data.documentType ? 'verified' : 'basic',
|
||||
verificationDate: new Date(),
|
||||
badges: [...profile.badges, ...DEFAULT_BADGES],
|
||||
roles: [ROLES.verified_user as Role],
|
||||
reputationScore: calculateReputationScore(
|
||||
[],
|
||||
data.documentType ? 'verified' : 'basic',
|
||||
[...profile.badges, ...DEFAULT_BADGES]
|
||||
)
|
||||
};
|
||||
|
||||
setProfile(updatedProfile);
|
||||
localStorage.setItem(`identity_${profile.address}`, JSON.stringify(updatedProfile));
|
||||
} catch (error) {
|
||||
console.error('KYC verification failed:', error);
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updatePrivacySettings = (settings: any) => {
|
||||
if (!profile) return;
|
||||
|
||||
const updatedProfile = {
|
||||
...profile,
|
||||
privacySettings: { ...profile.privacySettings, ...settings }
|
||||
};
|
||||
|
||||
setProfile(updatedProfile);
|
||||
localStorage.setItem(`identity_${profile.address}`, JSON.stringify(updatedProfile));
|
||||
};
|
||||
|
||||
const addBadge = (badge: Badge) => {
|
||||
if (!profile) return;
|
||||
|
||||
const updatedProfile = {
|
||||
...profile,
|
||||
badges: [...profile.badges, badge]
|
||||
};
|
||||
|
||||
setProfile(updatedProfile);
|
||||
localStorage.setItem(`identity_${profile.address}`, JSON.stringify(updatedProfile));
|
||||
};
|
||||
|
||||
const assignRole = (role: Role) => {
|
||||
if (!profile) return;
|
||||
|
||||
const updatedProfile = {
|
||||
...profile,
|
||||
roles: [...profile.roles, role]
|
||||
};
|
||||
|
||||
setProfile(updatedProfile);
|
||||
localStorage.setItem(`identity_${profile.address}`, JSON.stringify(updatedProfile));
|
||||
};
|
||||
|
||||
const refreshReputation = () => {
|
||||
if (!profile) return;
|
||||
|
||||
const newScore = calculateReputationScore([], profile.verificationLevel, profile.badges);
|
||||
const updatedProfile = { ...profile, reputationScore: newScore };
|
||||
|
||||
setProfile(updatedProfile);
|
||||
localStorage.setItem(`identity_${profile.address}`, JSON.stringify(updatedProfile));
|
||||
};
|
||||
|
||||
return (
|
||||
<IdentityContext.Provider value={{
|
||||
profile,
|
||||
isVerifying,
|
||||
startKYC,
|
||||
updatePrivacySettings,
|
||||
addBadge,
|
||||
assignRole,
|
||||
refreshReputation
|
||||
}}>
|
||||
{children}
|
||||
</IdentityContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useIdentity = () => {
|
||||
const context = useContext(IdentityContext);
|
||||
if (!context) {
|
||||
throw new Error('useIdentity must be used within IdentityProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -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 = 'wss://beta-rpc.pezkuwi.art' // Beta 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;
|
||||
};
|
||||
@@ -0,0 +1,286 @@
|
||||
// ========================================
|
||||
// WalletContext - Polkadot.js Wallet Integration
|
||||
// ========================================
|
||||
// This context wraps PolkadotContext and provides wallet functionality
|
||||
// ⚠️ MIGRATION NOTE: This now uses Polkadot.js instead of MetaMask/Ethereum
|
||||
|
||||
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { usePolkadot } from './PolkadotContext';
|
||||
import { WALLET_ERRORS, formatBalance, ASSET_IDS } from '@/lib/wallet';
|
||||
import type { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
|
||||
import type { Signer } from '@polkadot/api/types';
|
||||
import { web3FromAddress } from '@polkadot/extension-dapp';
|
||||
|
||||
interface TokenBalances {
|
||||
HEZ: string;
|
||||
PEZ: string;
|
||||
wHEZ: string;
|
||||
USDT: string; // User-facing key for wUSDT (backend uses wUSDT asset ID 2)
|
||||
}
|
||||
|
||||
interface WalletContextType {
|
||||
isConnected: boolean;
|
||||
account: string | null; // Current selected account address
|
||||
accounts: InjectedAccountWithMeta[];
|
||||
balance: string; // Legacy: HEZ balance
|
||||
balances: TokenBalances; // All token balances
|
||||
error: string | null;
|
||||
signer: Signer | null; // Polkadot.js signer for transactions
|
||||
connectWallet: () => Promise<void>;
|
||||
disconnect: () => void;
|
||||
switchAccount: (account: InjectedAccountWithMeta) => void;
|
||||
signTransaction: (tx: any) => Promise<string>;
|
||||
signMessage: (message: string) => Promise<string>;
|
||||
refreshBalances: () => Promise<void>; // Refresh all token balances
|
||||
}
|
||||
|
||||
const WalletContext = createContext<WalletContextType | undefined>(undefined);
|
||||
|
||||
export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const polkadot = usePolkadot();
|
||||
|
||||
console.log('🎯 WalletProvider render:', {
|
||||
hasApi: !!polkadot.api,
|
||||
isApiReady: polkadot.isApiReady,
|
||||
selectedAccount: polkadot.selectedAccount?.address,
|
||||
accountsCount: polkadot.accounts.length
|
||||
});
|
||||
|
||||
const [balance, setBalance] = useState<string>('0');
|
||||
const [balances, setBalances] = useState<TokenBalances>({ HEZ: '0', PEZ: '0', wHEZ: '0', USDT: '0' });
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [signer, setSigner] = useState<Signer | null>(null);
|
||||
|
||||
// Fetch all token balances when account changes
|
||||
const updateBalance = useCallback(async (address: string) => {
|
||||
if (!polkadot.api || !polkadot.isApiReady) {
|
||||
console.warn('API not ready, cannot fetch balance');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('💰 Fetching all token balances for:', address);
|
||||
|
||||
// Fetch HEZ (native token)
|
||||
const { data: nativeBalance } = await polkadot.api.query.system.account(address);
|
||||
const hezBalance = formatBalance(nativeBalance.free.toString());
|
||||
setBalance(hezBalance); // Legacy support
|
||||
|
||||
// Fetch PEZ (Asset ID: 1)
|
||||
let pezBalance = '0';
|
||||
try {
|
||||
const pezData = await polkadot.api.query.assets.account(ASSET_IDS.PEZ, address);
|
||||
console.log('📊 Raw PEZ data:', pezData.toHuman());
|
||||
|
||||
if (pezData.isSome) {
|
||||
const assetData = pezData.unwrap();
|
||||
const pezAmount = assetData.balance.toString();
|
||||
pezBalance = formatBalance(pezAmount);
|
||||
console.log('✅ PEZ balance found:', pezBalance);
|
||||
} else {
|
||||
console.warn('⚠️ PEZ asset not found for this account');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Failed to fetch PEZ balance:', err);
|
||||
}
|
||||
|
||||
// Fetch wHEZ (Asset ID: 0)
|
||||
let whezBalance = '0';
|
||||
try {
|
||||
const whezData = await polkadot.api.query.assets.account(ASSET_IDS.WHEZ, address);
|
||||
console.log('📊 Raw wHEZ data:', whezData.toHuman());
|
||||
|
||||
if (whezData.isSome) {
|
||||
const assetData = whezData.unwrap();
|
||||
const whezAmount = assetData.balance.toString();
|
||||
whezBalance = formatBalance(whezAmount);
|
||||
console.log('✅ wHEZ balance found:', whezBalance);
|
||||
} else {
|
||||
console.warn('⚠️ wHEZ asset not found for this account');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Failed to fetch wHEZ balance:', err);
|
||||
}
|
||||
|
||||
// Fetch wUSDT (Asset ID: 2) - IMPORTANT: wUSDT has 6 decimals, not 12!
|
||||
let wusdtBalance = '0';
|
||||
try {
|
||||
const wusdtData = await polkadot.api.query.assets.account(ASSET_IDS.WUSDT, address);
|
||||
console.log('📊 Raw wUSDT data:', wusdtData.toHuman());
|
||||
|
||||
if (wusdtData.isSome) {
|
||||
const assetData = wusdtData.unwrap();
|
||||
const wusdtAmount = assetData.balance.toString();
|
||||
wusdtBalance = formatBalance(wusdtAmount, 6); // wUSDT uses 6 decimals!
|
||||
console.log('✅ wUSDT balance found:', wusdtBalance);
|
||||
} else {
|
||||
console.warn('⚠️ wUSDT asset not found for this account');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('❌ Failed to fetch wUSDT balance:', err);
|
||||
}
|
||||
|
||||
setBalances({
|
||||
HEZ: hezBalance,
|
||||
PEZ: pezBalance,
|
||||
wHEZ: whezBalance,
|
||||
USDT: wusdtBalance,
|
||||
});
|
||||
|
||||
console.log('✅ Balances updated:', { HEZ: hezBalance, PEZ: pezBalance, wHEZ: whezBalance, wUSDT: wusdtBalance });
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch balances:', err);
|
||||
setError('Failed to fetch balances');
|
||||
}
|
||||
}, [polkadot.api, polkadot.isApiReady]);
|
||||
|
||||
// Connect wallet (Polkadot.js extension)
|
||||
const connectWallet = useCallback(async () => {
|
||||
try {
|
||||
setError(null);
|
||||
await polkadot.connectWallet();
|
||||
} catch (err: any) {
|
||||
console.error('Wallet connection failed:', err);
|
||||
setError(err.message || WALLET_ERRORS.CONNECTION_FAILED);
|
||||
}
|
||||
}, [polkadot]);
|
||||
|
||||
// Disconnect wallet
|
||||
const disconnect = useCallback(() => {
|
||||
polkadot.disconnectWallet();
|
||||
setBalance('0');
|
||||
setError(null);
|
||||
}, [polkadot]);
|
||||
|
||||
// Switch account
|
||||
const switchAccount = useCallback((account: InjectedAccountWithMeta) => {
|
||||
polkadot.setSelectedAccount(account);
|
||||
}, [polkadot]);
|
||||
|
||||
// Sign and submit transaction
|
||||
const signTransaction = useCallback(async (tx: any): Promise<string> => {
|
||||
if (!polkadot.api || !polkadot.selectedAccount) {
|
||||
throw new Error(WALLET_ERRORS.API_NOT_READY);
|
||||
}
|
||||
|
||||
try {
|
||||
const { web3FromAddress } = await import('@polkadot/extension-dapp');
|
||||
const injector = await web3FromAddress(polkadot.selectedAccount.address);
|
||||
|
||||
// Sign and send transaction
|
||||
const hash = await tx.signAndSend(
|
||||
polkadot.selectedAccount.address,
|
||||
{ signer: injector.signer }
|
||||
);
|
||||
|
||||
return hash.toHex();
|
||||
} catch (error: any) {
|
||||
console.error('Transaction failed:', error);
|
||||
throw new Error(error.message || WALLET_ERRORS.TRANSACTION_FAILED);
|
||||
}
|
||||
}, [polkadot.api, polkadot.selectedAccount]);
|
||||
|
||||
// Sign message
|
||||
const signMessage = useCallback(async (message: string): Promise<string> => {
|
||||
if (!polkadot.selectedAccount) {
|
||||
throw new Error('No account selected');
|
||||
}
|
||||
|
||||
try {
|
||||
const { web3FromAddress } = await import('@polkadot/extension-dapp');
|
||||
const injector = await web3FromAddress(polkadot.selectedAccount.address);
|
||||
|
||||
if (!injector.signer.signRaw) {
|
||||
throw new Error('Wallet does not support message signing');
|
||||
}
|
||||
|
||||
const { signature } = await injector.signer.signRaw({
|
||||
address: polkadot.selectedAccount.address,
|
||||
data: message,
|
||||
type: 'bytes'
|
||||
});
|
||||
|
||||
return signature;
|
||||
} catch (error: any) {
|
||||
console.error('Message signing failed:', error);
|
||||
throw new Error(error.message || 'Failed to sign message');
|
||||
}
|
||||
}, [polkadot.selectedAccount]);
|
||||
|
||||
// Get signer from extension when account changes
|
||||
useEffect(() => {
|
||||
const getSigner = async () => {
|
||||
if (polkadot.selectedAccount) {
|
||||
try {
|
||||
const injector = await web3FromAddress(polkadot.selectedAccount.address);
|
||||
setSigner(injector.signer);
|
||||
console.log('✅ Signer obtained for', polkadot.selectedAccount.address);
|
||||
} catch (error) {
|
||||
console.error('Failed to get signer:', error);
|
||||
setSigner(null);
|
||||
}
|
||||
} else {
|
||||
setSigner(null);
|
||||
}
|
||||
};
|
||||
|
||||
getSigner();
|
||||
}, [polkadot.selectedAccount]);
|
||||
|
||||
// Update balance when selected account changes
|
||||
useEffect(() => {
|
||||
console.log('🔄 WalletContext useEffect triggered!', {
|
||||
hasAccount: !!polkadot.selectedAccount,
|
||||
isApiReady: polkadot.isApiReady,
|
||||
address: polkadot.selectedAccount?.address
|
||||
});
|
||||
|
||||
if (polkadot.selectedAccount && polkadot.isApiReady) {
|
||||
updateBalance(polkadot.selectedAccount.address);
|
||||
}
|
||||
}, [polkadot.selectedAccount, polkadot.isApiReady]);
|
||||
|
||||
// Sync error state with PolkadotContext
|
||||
useEffect(() => {
|
||||
if (polkadot.error) {
|
||||
setError(polkadot.error);
|
||||
}
|
||||
}, [polkadot.error]);
|
||||
|
||||
// Refresh balances for current account
|
||||
const refreshBalances = useCallback(async () => {
|
||||
if (polkadot.selectedAccount) {
|
||||
await updateBalance(polkadot.selectedAccount.address);
|
||||
}
|
||||
}, [polkadot.selectedAccount, updateBalance]);
|
||||
|
||||
const value: WalletContextType = {
|
||||
isConnected: polkadot.accounts.length > 0,
|
||||
account: polkadot.selectedAccount?.address || null,
|
||||
accounts: polkadot.accounts,
|
||||
balance,
|
||||
balances,
|
||||
error: error || polkadot.error,
|
||||
signer,
|
||||
connectWallet,
|
||||
disconnect,
|
||||
switchAccount,
|
||||
signTransaction,
|
||||
signMessage,
|
||||
refreshBalances,
|
||||
};
|
||||
|
||||
return (
|
||||
<WalletContext.Provider value={value}>
|
||||
{children}
|
||||
</WalletContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useWallet = () => {
|
||||
const context = useContext(WalletContext);
|
||||
if (!context) {
|
||||
throw new Error('useWallet must be used within WalletProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -0,0 +1,171 @@
|
||||
import React, { createContext, useContext, useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
|
||||
interface WebSocketMessage {
|
||||
type: 'comment' | 'vote' | 'sentiment' | 'mention' | 'reply' | 'proposal_update';
|
||||
data: any;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface WebSocketContextType {
|
||||
isConnected: boolean;
|
||||
subscribe: (event: string, callback: (data: any) => void) => void;
|
||||
unsubscribe: (event: string, callback: (data: any) => void) => void;
|
||||
sendMessage: (message: WebSocketMessage) => void;
|
||||
reconnect: () => void;
|
||||
}
|
||||
|
||||
const WebSocketContext = createContext<WebSocketContextType | null>(null);
|
||||
|
||||
export const useWebSocket = () => {
|
||||
const context = useContext(WebSocketContext);
|
||||
if (!context) {
|
||||
throw new Error('useWebSocket must be used within WebSocketProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
const reconnectTimeout = useRef<NodeJS.Timeout>();
|
||||
const eventListeners = useRef<Map<string, Set<(data: any) => void>>>(new Map());
|
||||
const { toast } = useToast();
|
||||
|
||||
// Connection state management
|
||||
const currentEndpoint = useRef<string>('');
|
||||
const hasShownFinalError = useRef(false);
|
||||
const connectionAttempts = useRef(0);
|
||||
|
||||
const ENDPOINTS = [
|
||||
'wss://ws.pezkuwichain.io', // Production WebSocket
|
||||
'ws://localhost:9944', // Local development node
|
||||
'ws://127.0.0.1:9944', // Alternative local address
|
||||
];
|
||||
|
||||
const connect = useCallback((endpointIndex: number = 0) => {
|
||||
// If we've tried all endpoints, show error once and stop
|
||||
if (endpointIndex >= ENDPOINTS.length) {
|
||||
if (!hasShownFinalError.current) {
|
||||
console.error('❌ All WebSocket endpoints failed');
|
||||
toast({
|
||||
title: "Real-time Connection Unavailable",
|
||||
description: "Could not connect to WebSocket server. Live updates will be disabled.",
|
||||
variant: "destructive",
|
||||
});
|
||||
hasShownFinalError.current = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const wsUrl = ENDPOINTS[endpointIndex];
|
||||
currentEndpoint.current = wsUrl;
|
||||
|
||||
console.log(`🔌 Attempting WebSocket connection to: ${wsUrl}`);
|
||||
|
||||
ws.current = new WebSocket(wsUrl);
|
||||
|
||||
ws.current.onopen = () => {
|
||||
setIsConnected(true);
|
||||
connectionAttempts.current = 0;
|
||||
hasShownFinalError.current = false;
|
||||
console.log(`✅ WebSocket connected to: ${wsUrl}`);
|
||||
|
||||
// Only show success toast for production endpoint
|
||||
if (endpointIndex === 0) {
|
||||
toast({
|
||||
title: "Connected",
|
||||
description: "Real-time updates enabled",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ws.current.onmessage = (event) => {
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(event.data);
|
||||
const listeners = eventListeners.current.get(message.type);
|
||||
if (listeners) {
|
||||
listeners.forEach(callback => callback(message.data));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
ws.current.onerror = (error) => {
|
||||
console.warn(`⚠️ WebSocket error on ${wsUrl}:`, error);
|
||||
};
|
||||
|
||||
ws.current.onclose = () => {
|
||||
setIsConnected(false);
|
||||
console.log(`🔌 WebSocket disconnected from: ${wsUrl}`);
|
||||
|
||||
// Try next endpoint after 2 seconds
|
||||
reconnectTimeout.current = setTimeout(() => {
|
||||
connectionAttempts.current++;
|
||||
|
||||
// If we've been connected before and lost connection, try same endpoint first
|
||||
if (connectionAttempts.current < 3) {
|
||||
connect(endpointIndex);
|
||||
} else {
|
||||
// Try next endpoint in the list
|
||||
connect(endpointIndex + 1);
|
||||
connectionAttempts.current = 0;
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to create WebSocket connection to ${ENDPOINTS[endpointIndex]}:`, error);
|
||||
// Try next endpoint immediately
|
||||
setTimeout(() => connect(endpointIndex + 1), 1000);
|
||||
}
|
||||
}, [toast]);
|
||||
|
||||
useEffect(() => {
|
||||
connect(0); // Start with first endpoint
|
||||
|
||||
return () => {
|
||||
if (reconnectTimeout.current) {
|
||||
clearTimeout(reconnectTimeout.current);
|
||||
}
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
}
|
||||
};
|
||||
}, [connect]);
|
||||
|
||||
const subscribe = useCallback((event: string, callback: (data: any) => void) => {
|
||||
if (!eventListeners.current.has(event)) {
|
||||
eventListeners.current.set(event, new Set());
|
||||
}
|
||||
eventListeners.current.get(event)?.add(callback);
|
||||
}, []);
|
||||
|
||||
const unsubscribe = useCallback((event: string, callback: (data: any) => void) => {
|
||||
eventListeners.current.get(event)?.delete(callback);
|
||||
}, []);
|
||||
|
||||
const sendMessage = useCallback((message: WebSocketMessage) => {
|
||||
if (ws.current?.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.warn('WebSocket is not connected - message queued');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reconnect = useCallback(() => {
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
}
|
||||
hasShownFinalError.current = false;
|
||||
connectionAttempts.current = 0;
|
||||
connect(0); // Start from first endpoint again
|
||||
}, [connect]);
|
||||
|
||||
return (
|
||||
<WebSocketContext.Provider value={{ isConnected, subscribe, unsubscribe, sendMessage, reconnect }}>
|
||||
{children}
|
||||
</WebSocketContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user