mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-25 16:28:02 +00:00
feat: add WalletConnect v2 integration for mobile wallet connection
- Add @walletconnect/sign-client for dApp-side WC v2 support - Create walletconnect-service.ts with session management and Signer adapter - Create WalletConnectModal.tsx with QR code display - Update PezkuwiContext with connectWalletConnect(), session restore, walletSource tracking - Update WalletContext signing router: mobile → WalletConnect → extension - Update WalletModal with "Connect with pezWallet (Mobile)" button - Move WalletConnect projectId to env variable - Fix vite build: disable assetsInclude for JSON (crypto-browserify compat)
This commit is contained in:
@@ -26,6 +26,16 @@ import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
|
||||
import { DEFAULT_ENDPOINT } from '../../../shared/blockchain/pezkuwi';
|
||||
import { getCurrentNetworkConfig } from '../../../shared/blockchain/endpoints';
|
||||
import { isMobileApp, getNativeWalletAddress, getNativeAccountName } from '@/lib/mobile-bridge';
|
||||
import {
|
||||
initWalletConnect,
|
||||
connectWithQR,
|
||||
getSessionAccounts,
|
||||
getSessionPeerName,
|
||||
restoreSession,
|
||||
disconnectWC,
|
||||
isWCConnected,
|
||||
createWCSigner,
|
||||
} from '@/lib/walletconnect-service';
|
||||
|
||||
// Get network config from shared endpoints
|
||||
const networkConfig = getCurrentNetworkConfig();
|
||||
@@ -34,6 +44,8 @@ const networkConfig = getCurrentNetworkConfig();
|
||||
const ASSET_HUB_ENDPOINT = import.meta.env.VITE_ASSET_HUB_ENDPOINT || networkConfig.assetHubEndpoint || 'wss://asset-hub-rpc.pezkuwichain.io';
|
||||
const PEOPLE_CHAIN_ENDPOINT = import.meta.env.VITE_PEOPLE_CHAIN_ENDPOINT || networkConfig.peopleChainEndpoint || 'wss://people-rpc.pezkuwichain.io';
|
||||
|
||||
export type WalletSource = 'extension' | 'native' | 'walletconnect' | null;
|
||||
|
||||
interface PezkuwiContextType {
|
||||
api: ApiPromise | null;
|
||||
assetHubApi: ApiPromise | null;
|
||||
@@ -46,7 +58,10 @@ interface PezkuwiContextType {
|
||||
selectedAccount: InjectedAccountWithMeta | null;
|
||||
setSelectedAccount: (account: InjectedAccountWithMeta | null) => void;
|
||||
connectWallet: () => Promise<void>;
|
||||
connectWalletConnect: () => Promise<string>;
|
||||
disconnectWallet: () => void;
|
||||
walletSource: WalletSource;
|
||||
wcPeerName: string | null;
|
||||
error: string | null;
|
||||
sudoKey: string | null;
|
||||
}
|
||||
@@ -72,6 +87,8 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
const [selectedAccount, setSelectedAccount] = useState<InjectedAccountWithMeta | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [sudoKey, setSudoKey] = useState<string | null>(null);
|
||||
const [walletSource, setWalletSource] = useState<WalletSource>(null);
|
||||
const [wcPeerName, setWcPeerName] = useState<string | null>(null);
|
||||
|
||||
// Wrapper to trigger events when wallet changes
|
||||
const handleSetSelectedAccount = (account: InjectedAccountWithMeta | null) => {
|
||||
@@ -310,12 +327,44 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop: Try to restore from localStorage
|
||||
// Try to restore WalletConnect session first
|
||||
try {
|
||||
const wcSession = await restoreSession();
|
||||
if (wcSession) {
|
||||
const wcAddresses = getSessionAccounts();
|
||||
if (wcAddresses.length > 0) {
|
||||
const peerName = getSessionPeerName();
|
||||
const wcAccounts: InjectedAccountWithMeta[] = wcAddresses.map((addr) => ({
|
||||
address: addr,
|
||||
meta: {
|
||||
name: peerName || 'WalletConnect',
|
||||
source: 'walletconnect',
|
||||
},
|
||||
type: 'sr25519' as const,
|
||||
}));
|
||||
|
||||
setAccounts(wcAccounts);
|
||||
handleSetSelectedAccount(wcAccounts[0]);
|
||||
setWalletSource('walletconnect');
|
||||
setWcPeerName(peerName);
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('✅ WalletConnect session restored:', wcAddresses[0].slice(0, 8) + '...');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn('Failed to restore WC session:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop: Try to restore from localStorage (extension)
|
||||
const savedAddress = localStorage.getItem('selectedWallet');
|
||||
if (!savedAddress) return;
|
||||
|
||||
try {
|
||||
// Enable extension
|
||||
// Enable extension (works for both desktop extension and pezWallet DApps browser)
|
||||
const extensions = await web3Enable('PezkuwiChain');
|
||||
if (extensions.length === 0) return;
|
||||
|
||||
@@ -328,6 +377,7 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
if (savedAccount) {
|
||||
setAccounts(allAccounts);
|
||||
handleSetSelectedAccount(savedAccount);
|
||||
setWalletSource('extension');
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('✅ Wallet restored:', savedAddress.slice(0, 8) + '...');
|
||||
}
|
||||
@@ -361,13 +411,12 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
// Check if running in mobile app
|
||||
// Check if running in mobile app (native WebView bridge)
|
||||
if (isMobileApp()) {
|
||||
const nativeAddress = getNativeWalletAddress();
|
||||
const nativeAccountName = getNativeAccountName();
|
||||
|
||||
if (nativeAddress) {
|
||||
// Create a virtual account for the mobile wallet
|
||||
const mobileAccount: InjectedAccountWithMeta = {
|
||||
address: nativeAddress,
|
||||
meta: {
|
||||
@@ -379,30 +428,27 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
|
||||
setAccounts([mobileAccount]);
|
||||
handleSetSelectedAccount(mobileAccount);
|
||||
setWalletSource('native');
|
||||
|
||||
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
|
||||
// Desktop / pezWallet DApps browser: Try extension (injected provider)
|
||||
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');
|
||||
}
|
||||
@@ -413,7 +459,6 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
console.log('✅ Pezkuwi.js extension enabled');
|
||||
}
|
||||
|
||||
// Get accounts
|
||||
const allAccounts = await web3Accounts();
|
||||
|
||||
if (allAccounts.length === 0) {
|
||||
@@ -423,14 +468,13 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
|
||||
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);
|
||||
setWalletSource('extension');
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(`✅ Found ${allAccounts.length} account(s)`);
|
||||
@@ -444,12 +488,67 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Disconnect wallet
|
||||
const disconnectWallet = () => {
|
||||
// Connect via WalletConnect v2 - returns pairing URI for QR code
|
||||
const connectWalletConnect = async (): Promise<string> => {
|
||||
if (!api || !isApiReady) {
|
||||
throw new Error('API not ready. Please wait for blockchain connection.');
|
||||
}
|
||||
|
||||
setError(null);
|
||||
const genesisHash = api.genesisHash.toHex();
|
||||
|
||||
try {
|
||||
await initWalletConnect();
|
||||
const { uri, approval } = await connectWithQR(genesisHash);
|
||||
|
||||
// Start approval listener in background
|
||||
approval().then((session) => {
|
||||
const wcAddresses = getSessionAccounts();
|
||||
if (wcAddresses.length > 0) {
|
||||
const peerName = getSessionPeerName();
|
||||
const wcAccounts: InjectedAccountWithMeta[] = wcAddresses.map((addr) => ({
|
||||
address: addr,
|
||||
meta: {
|
||||
name: peerName || 'WalletConnect',
|
||||
source: 'walletconnect',
|
||||
},
|
||||
type: 'sr25519' as const,
|
||||
}));
|
||||
|
||||
setAccounts(wcAccounts);
|
||||
handleSetSelectedAccount(wcAccounts[0]);
|
||||
setWalletSource('walletconnect');
|
||||
setWcPeerName(peerName);
|
||||
window.dispatchEvent(new Event('walletconnect_connected'));
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('✅ WalletConnect session established:', session.topic);
|
||||
}
|
||||
}
|
||||
}).catch((err) => {
|
||||
if (import.meta.env.DEV) console.error('WalletConnect approval failed:', err);
|
||||
setError('WalletConnect connection was rejected');
|
||||
});
|
||||
|
||||
return uri;
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('WalletConnect connection failed:', err);
|
||||
setError('Failed to start WalletConnect');
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// Disconnect wallet (extension, native, or WalletConnect)
|
||||
const disconnectWallet = async () => {
|
||||
if (walletSource === 'walletconnect') {
|
||||
await disconnectWC();
|
||||
setWcPeerName(null);
|
||||
}
|
||||
setAccounts([]);
|
||||
handleSetSelectedAccount(null);
|
||||
setWalletSource(null);
|
||||
if (import.meta.env.DEV) {
|
||||
if (import.meta.env.DEV) console.log('🔌 Wallet disconnected');
|
||||
console.log('🔌 Wallet disconnected');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -460,12 +559,15 @@ export const PezkuwiProvider: React.FC<PezkuwiProviderProps> = ({
|
||||
isApiReady,
|
||||
isAssetHubReady,
|
||||
isPeopleReady,
|
||||
isConnected: isApiReady, // Alias for backward compatibility
|
||||
isConnected: isApiReady,
|
||||
accounts,
|
||||
selectedAccount,
|
||||
setSelectedAccount: handleSetSelectedAccount,
|
||||
connectWallet,
|
||||
connectWalletConnect,
|
||||
disconnectWallet,
|
||||
walletSource,
|
||||
wcPeerName,
|
||||
error,
|
||||
sudoKey,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { InjectedAccountWithMeta } from '@pezkuwi/extension-inject/types';
|
||||
import type { Signer } from '@pezkuwi/api/types';
|
||||
import { web3FromAddress } from '@pezkuwi/extension-dapp';
|
||||
import { isMobileApp, signTransactionNative, type TransactionPayload } from '@/lib/mobile-bridge';
|
||||
import { createWCSigner, isWCConnected } from '@/lib/walletconnect-service';
|
||||
|
||||
interface TokenBalances {
|
||||
HEZ: string;
|
||||
@@ -255,11 +256,25 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
return blockHash;
|
||||
}
|
||||
|
||||
// Desktop: Use browser extension for signing
|
||||
// WalletConnect: Use WC signer
|
||||
if (pezkuwi.walletSource === 'walletconnect' && isWCConnected() && pezkuwi.api) {
|
||||
if (import.meta.env.DEV) console.log('[WC] Using WalletConnect for transaction signing');
|
||||
|
||||
const genesisHash = pezkuwi.api.genesisHash.toHex();
|
||||
const wcSigner = createWCSigner(genesisHash, pezkuwi.selectedAccount.address);
|
||||
|
||||
const hash = await (tx as { signAndSend: (address: string, options: { signer: unknown }) => Promise<{ toHex: () => string }> }).signAndSend(
|
||||
pezkuwi.selectedAccount.address,
|
||||
{ signer: wcSigner }
|
||||
);
|
||||
|
||||
return hash.toHex();
|
||||
}
|
||||
|
||||
// Desktop / pezWallet DApps browser: Use extension signer
|
||||
const { web3FromAddress } = await import('@pezkuwi/extension-dapp');
|
||||
const injector = await web3FromAddress(pezkuwi.selectedAccount.address);
|
||||
|
||||
// Sign and send transaction
|
||||
const hash = await (tx as { signAndSend: (address: string, options: { signer: unknown }) => Promise<{ toHex: () => string }> }).signAndSend(
|
||||
pezkuwi.selectedAccount.address,
|
||||
{ signer: injector.signer }
|
||||
@@ -279,6 +294,23 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
}
|
||||
|
||||
try {
|
||||
// WalletConnect signing
|
||||
if (pezkuwi.walletSource === 'walletconnect' && isWCConnected() && pezkuwi.api) {
|
||||
if (import.meta.env.DEV) console.log('[WC] Using WalletConnect for message signing');
|
||||
|
||||
const genesisHash = pezkuwi.api.genesisHash.toHex();
|
||||
const wcSigner = createWCSigner(genesisHash, pezkuwi.selectedAccount.address);
|
||||
|
||||
const { signature } = await wcSigner.signRaw({
|
||||
address: pezkuwi.selectedAccount.address,
|
||||
data: message,
|
||||
type: 'bytes',
|
||||
});
|
||||
|
||||
return signature;
|
||||
}
|
||||
|
||||
// Extension signing
|
||||
const { web3FromAddress } = await import('@pezkuwi/extension-dapp');
|
||||
const injector = await web3FromAddress(pezkuwi.selectedAccount.address);
|
||||
|
||||
@@ -297,27 +329,35 @@ export const WalletProvider: React.FC<{ children: React.ReactNode }> = ({ childr
|
||||
if (import.meta.env.DEV) console.error('Message signing failed:', error);
|
||||
throw new Error(error instanceof Error ? error.message : 'Failed to sign message');
|
||||
}
|
||||
}, [pezkuwi.selectedAccount]);
|
||||
}, [pezkuwi.selectedAccount, pezkuwi.walletSource, pezkuwi.api]);
|
||||
|
||||
// Get signer from extension when account changes
|
||||
// Get signer from extension or WalletConnect when account changes
|
||||
useEffect(() => {
|
||||
const getSigner = async () => {
|
||||
if (pezkuwi.selectedAccount) {
|
||||
try {
|
||||
if (!pezkuwi.selectedAccount) {
|
||||
setSigner(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (pezkuwi.walletSource === 'walletconnect' && isWCConnected() && pezkuwi.api) {
|
||||
const genesisHash = pezkuwi.api.genesisHash.toHex();
|
||||
const wcSigner = createWCSigner(genesisHash, pezkuwi.selectedAccount.address);
|
||||
setSigner(wcSigner as unknown as Signer);
|
||||
if (import.meta.env.DEV) console.log('✅ WC Signer obtained for', pezkuwi.selectedAccount.address);
|
||||
} else {
|
||||
const injector = await web3FromAddress(pezkuwi.selectedAccount.address);
|
||||
setSigner(injector.signer);
|
||||
if (import.meta.env.DEV) console.log('✅ Signer obtained for', pezkuwi.selectedAccount.address);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Failed to get signer:', error);
|
||||
setSigner(null);
|
||||
if (import.meta.env.DEV) console.log('✅ Extension Signer obtained for', pezkuwi.selectedAccount.address);
|
||||
}
|
||||
} else {
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) console.error('Failed to get signer:', error);
|
||||
setSigner(null);
|
||||
}
|
||||
};
|
||||
|
||||
getSigner();
|
||||
}, [pezkuwi.selectedAccount]);
|
||||
}, [pezkuwi.selectedAccount, pezkuwi.walletSource, pezkuwi.api]);
|
||||
|
||||
// Update balance when selected account changes or Asset Hub becomes ready
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user