mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 04:27:56 +00:00
451 lines
15 KiB
TypeScript
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;
|
|
};
|