Files
pwap/web/src/contexts/PezkuwiContext.tsx
T

451 lines
15 KiB
TypeScript

import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { ApiPromise, WsProvider } from '@pezkuwi/api';
import { web3Accounts, web3Enable } from '@pezkuwi/extension-dapp';
import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
import { DEFAULT_ENDPOINT } from '../../../shared/blockchain/pezkuwi';
import { isMobileApp, getNativeWalletAddress, getNativeAccountName } from '@/lib/mobile-bridge';
// Teyrchain endpoints (from environment or defaults)
const ASSET_HUB_ENDPOINT = import.meta.env.VITE_ASSET_HUB_ENDPOINT || 'wss://asset-hub-rpc.pezkuwichain.io';
const PEOPLE_CHAIN_ENDPOINT = import.meta.env.VITE_PEOPLE_CHAIN_ENDPOINT || 'wss://people-rpc.pezkuwichain.io';
interface PezkuwiContextType {
api: ApiPromise | null;
assetHubApi: ApiPromise | null;
peopleApi: ApiPromise | null;
isApiReady: boolean;
isAssetHubReady: boolean;
isPeopleReady: boolean;
isConnected: boolean;
accounts: InjectedAccountWithMeta[];
selectedAccount: InjectedAccountWithMeta | null;
setSelectedAccount: (account: InjectedAccountWithMeta | null) => void;
connectWallet: () => Promise<void>;
disconnectWallet: () => void;
error: string | null;
sudoKey: string | null;
}
const PezkuwiContext = createContext<PezkuwiContextType | undefined>(undefined);
interface PezkuwiProviderProps {
children: ReactNode;
endpoint?: string;
}
export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
children,
endpoint = DEFAULT_ENDPOINT // Beta testnet RPC from shared config
}) => {
const [api, setApi] = useState<ApiPromise | null>(null);
const [assetHubApi, setAssetHubApi] = useState<ApiPromise | null>(null);
const [peopleApi, setPeopleApi] = useState<ApiPromise | null>(null);
const [isApiReady, setIsApiReady] = useState(false);
const [isAssetHubReady, setIsAssetHubReady] = useState(false);
const [isPeopleReady, setIsPeopleReady] = useState(false);
const [accounts, setAccounts] = useState<InjectedAccountWithMeta[]>([]);
const [selectedAccount, setSelectedAccount] = useState<InjectedAccountWithMeta | null>(null);
const [error, setError] = useState<string | null>(null);
const [sudoKey, setSudoKey] = useState<string | null>(null);
// Wrapper to trigger events when wallet changes
const handleSetSelectedAccount = (account: InjectedAccountWithMeta | null) => {
setSelectedAccount(account);
if (account) {
localStorage.setItem('selectedWallet', account.address);
if (import.meta.env.DEV) {
if (import.meta.env.DEV) console.log('💾 Wallet saved:', account.address);
}
window.dispatchEvent(new Event('walletChanged'));
} else {
localStorage.removeItem('selectedWallet');
window.dispatchEvent(new Event('walletChanged'));
}
};
// Initialize Pezkuwi API with fallback endpoints
useEffect(() => {
// Hardcoded production fallbacks ensure app works even if env vars are missing
const PRODUCTION_FALLBACKS = [
'wss://rpc.pezkuwichain.io',
'wss://mainnet.pezkuwichain.io',
];
const FALLBACK_ENDPOINTS = [
endpoint,
import.meta.env.VITE_WS_ENDPOINT_FALLBACK_1,
import.meta.env.VITE_WS_ENDPOINT_FALLBACK_2,
...PRODUCTION_FALLBACKS,
].filter((ep, index, arr) => ep && arr.indexOf(ep) === index); // Remove duplicates and empty
const initApi = async () => {
let lastError: unknown = null;
for (const currentEndpoint of FALLBACK_ENDPOINTS) {
try {
if (import.meta.env.DEV) {
if (import.meta.env.DEV) console.log('🔗 Connecting to Pezkuwi node:', currentEndpoint);
}
const provider = new WsProvider(currentEndpoint);
// PezkuwiChain custom signed extensions
const apiInstance = await ApiPromise.create({
provider,
signedExtensions: {
AuthorizeCall: {
extrinsic: {},
payload: {},
},
},
});
await apiInstance.isReady;
setApi(apiInstance);
setIsApiReady(true);
setError(null);
if (import.meta.env.DEV) {
if (import.meta.env.DEV) 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(),
]);
if (import.meta.env.DEV) console.log(`📡 Chain: ${chain}`);
if (import.meta.env.DEV) console.log(`🖥️ Node: ${nodeName} v${nodeVersion}`);
// Debug: Check Junction type definition
try {
const junctionType = apiInstance.createType('XcmV3Junction');
const junctionObj = junctionType as unknown as { defKeys?: string[] };
console.log('🔍 XCM Junction type keys:', junctionObj.defKeys || Object.keys(junctionType.toJSON() || {}));
// Expose api for console debugging
(window as unknown as { __PEZKUWI_API__: typeof apiInstance }).__PEZKUWI_API__ = apiInstance;
console.log('💡 API exposed as window.__PEZKUWI_API__ for debugging');
} catch (e) {
console.log('⚠️ Could not check Junction type:', e);
}
}
// Fetch sudo key from blockchain
try {
const sudoAccount = await apiInstance.query.sudo.key();
const sudoAddress = sudoAccount.toString();
setSudoKey(sudoAddress);
if (import.meta.env.DEV) console.log(`🔑 Sudo key: ${sudoAddress}`);
} catch (err) {
if (import.meta.env.DEV) console.warn('⚠️ Failed to fetch sudo key (sudo pallet may not be available):', err);
}
return;
} catch (err) {
lastError = err;
if (import.meta.env.DEV) {
if (import.meta.env.DEV) console.warn(`⚠️ Failed to connect to ${currentEndpoint}, trying next...`);
}
continue;
}
}
if (import.meta.env.DEV) {
if (import.meta.env.DEV) console.error('❌ Failed to connect to all endpoints:', lastError);
}
setError('Failed to connect to blockchain network. Please try again later.');
setIsApiReady(false);
};
// Initialize Asset Hub API for PEZ token
const initAssetHubApi = async () => {
try {
if (import.meta.env.DEV) {
console.log('🔗 Connecting to Asset Hub:', ASSET_HUB_ENDPOINT);
}
const provider = new WsProvider(ASSET_HUB_ENDPOINT);
const assetHubApiInstance = await ApiPromise.create({
provider,
signedExtensions: {
AuthorizeCall: {
extrinsic: {},
payload: {},
},
},
});
await assetHubApiInstance.isReady;
setAssetHubApi(assetHubApiInstance);
setIsAssetHubReady(true);
if (import.meta.env.DEV) {
console.log('✅ Connected to Asset Hub for PEZ token');
}
} catch (err) {
if (import.meta.env.DEV) {
console.error('❌ Failed to connect to Asset Hub:', err);
}
// Don't set error - PEZ features just won't work
}
};
// Initialize People Chain API for identity/citizenship
const initPeopleApi = async () => {
try {
if (import.meta.env.DEV) {
console.log('🔗 Connecting to People Chain:', PEOPLE_CHAIN_ENDPOINT);
}
const provider = new WsProvider(PEOPLE_CHAIN_ENDPOINT);
const peopleApiInstance = await ApiPromise.create({
provider,
signedExtensions: {
AuthorizeCall: {
extrinsic: {},
payload: {},
},
},
});
await peopleApiInstance.isReady;
setPeopleApi(peopleApiInstance);
setIsPeopleReady(true);
if (import.meta.env.DEV) {
console.log('✅ Connected to People Chain for identity');
}
} catch (err) {
if (import.meta.env.DEV) {
console.error('❌ Failed to connect to People Chain:', err);
}
// Don't set error - Identity features just won't work
}
};
initApi();
initAssetHubApi();
initPeopleApi();
return () => {
if (api) {
api.disconnect();
}
if (assetHubApi) {
assetHubApi.disconnect();
}
if (peopleApi) {
peopleApi.disconnect();
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [endpoint]);
// Auto-restore wallet on page load (or setup mobile wallet)
useEffect(() => {
const restoreWallet = async () => {
// Check if running in mobile app
if (isMobileApp()) {
const nativeAddress = getNativeWalletAddress();
const nativeAccountName = getNativeAccountName();
if (nativeAddress) {
// Create a virtual account for the mobile wallet
const mobileAccount: InjectedAccountWithMeta = {
address: nativeAddress,
meta: {
name: nativeAccountName || 'Mobile Wallet',
source: 'pezkuwi-mobile',
},
type: 'sr25519',
};
setAccounts([mobileAccount]);
handleSetSelectedAccount(mobileAccount);
if (import.meta.env.DEV) {
console.log('[Mobile] Native wallet connected:', nativeAddress.slice(0, 8) + '...');
}
return;
}
}
// Desktop: Try to restore from localStorage
const savedAddress = localStorage.getItem('selectedWallet');
if (!savedAddress) return;
try {
// Enable extension
const extensions = await web3Enable('PezkuwiChain');
if (extensions.length === 0) return;
// Get accounts
const allAccounts = await web3Accounts();
if (allAccounts.length === 0) return;
// Find saved account
const savedAccount = allAccounts.find(acc => acc.address === savedAddress);
if (savedAccount) {
setAccounts(allAccounts);
handleSetSelectedAccount(savedAccount);
if (import.meta.env.DEV) {
console.log('✅ Wallet restored:', savedAddress.slice(0, 8) + '...');
}
}
} catch (err) {
if (import.meta.env.DEV) {
console.error('Failed to restore wallet:', err);
}
}
};
restoreWallet();
// Listen for native bridge ready event (mobile)
const handleNativeReady = () => {
if (import.meta.env.DEV) {
console.log('[Mobile] Native bridge ready, restoring wallet');
}
restoreWallet();
};
window.addEventListener('pezkuwi-native-ready', handleNativeReady);
return () => {
window.removeEventListener('pezkuwi-native-ready', handleNativeReady);
};
}, []);
// Connect wallet (Pezkuwi.js extension or native mobile)
const connectWallet = async () => {
try {
setError(null);
// Check if running in mobile app
if (isMobileApp()) {
const nativeAddress = getNativeWalletAddress();
const nativeAccountName = getNativeAccountName();
if (nativeAddress) {
// Create a virtual account for the mobile wallet
const mobileAccount: InjectedAccountWithMeta = {
address: nativeAddress,
meta: {
name: nativeAccountName || 'Mobile Wallet',
source: 'pezkuwi-mobile',
},
type: 'sr25519',
};
setAccounts([mobileAccount]);
handleSetSelectedAccount(mobileAccount);
if (import.meta.env.DEV) {
console.log('[Mobile] Native wallet connected:', nativeAddress.slice(0, 8) + '...');
}
return;
} else {
// Request wallet connection from native app
setError('Please connect your wallet in the app');
return;
}
}
// Desktop: Check if extension is installed first
const hasExtension = !!(window as unknown as { injectedWeb3?: Record<string, unknown> }).injectedWeb3;
// Enable extension
const extensions = await web3Enable('PezkuwiChain');
if (extensions.length === 0) {
if (hasExtension) {
// Extension is installed but user didn't authorize - don't redirect
setError('Please authorize the connection in your Pezkuwi Wallet extension');
} else {
// Extension not installed - show install link
setError('Pezkuwi Wallet extension not found. Please install from Chrome Web Store.');
window.open('https://chrome.google.com/webstore/detail/pezkuwi-wallet/fbnboicjjeebjhgnapneaeccpgjcdibn', '_blank');
}
return;
}
if (import.meta.env.DEV) {
console.log('✅ Pezkuwi.js extension enabled');
}
// Get accounts
const allAccounts = await web3Accounts();
if (allAccounts.length === 0) {
setError('No accounts found. Please create an account in Pezkuwi.js extension');
return;
}
setAccounts(allAccounts);
// Try to restore previously selected account, otherwise use first
const savedAddress = localStorage.getItem('selectedWallet');
const accountToSelect = savedAddress
? allAccounts.find(acc => acc.address === savedAddress) || allAccounts[0]
: allAccounts[0];
// Use wrapper to trigger events
handleSetSelectedAccount(accountToSelect);
if (import.meta.env.DEV) {
console.log(`✅ Found ${allAccounts.length} account(s)`);
}
} catch (err) {
if (import.meta.env.DEV) {
console.error('❌ Wallet connection failed:', err);
}
setError('Failed to connect wallet');
}
};
// Disconnect wallet
const disconnectWallet = () => {
setAccounts([]);
handleSetSelectedAccount(null);
if (import.meta.env.DEV) {
if (import.meta.env.DEV) console.log('🔌 Wallet disconnected');
}
};
const value: PezkuwiContextType = {
api,
assetHubApi,
peopleApi,
isApiReady,
isAssetHubReady,
isPeopleReady,
isConnected: isApiReady, // Alias for backward compatibility
accounts,
selectedAccount,
setSelectedAccount: handleSetSelectedAccount,
connectWallet,
disconnectWallet,
error,
sudoKey,
};
return (
<PezkuwiContext.Provider value={value}>
{children}
</PezkuwiContext.Provider>
);
};
// Hook to use Pezkuwi context
export const usePezkuwi = (): PezkuwiContextType => {
const context = useContext(PezkuwiContext);
if (!context) {
throw new Error('usePezkuwi must be used within PezkuwiProvider');
}
return context;
};