mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-21 23:47:56 +00:00
fix(web): guard messaging against missing pallet + add back-to-home
- Check isPalletAvailable() BEFORE requesting wallet signature - All chain queries return safe defaults if pallet not in runtime - Show orange banner when messaging pallet needs runtime upgrade - Add floating back-to-home button on messaging page
This commit is contained in:
@@ -3,6 +3,7 @@ import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useWallet } from '@/contexts/WalletContext';
|
||||
import { deriveKeypair, encryptMessage, decryptMessage } from '@/lib/messaging/crypto';
|
||||
import {
|
||||
isPalletAvailable,
|
||||
getEncryptionKey,
|
||||
getCurrentEra,
|
||||
getInbox,
|
||||
@@ -23,6 +24,7 @@ export interface DecryptedMessage {
|
||||
}
|
||||
|
||||
interface MessagingState {
|
||||
palletReady: boolean;
|
||||
isKeyRegistered: boolean;
|
||||
isKeyUnlocked: boolean;
|
||||
inbox: EncryptedMessage[];
|
||||
@@ -39,6 +41,7 @@ export function useMessaging() {
|
||||
const { signMessage } = useWallet();
|
||||
|
||||
const [state, setState] = useState<MessagingState>({
|
||||
palletReady: false,
|
||||
isKeyRegistered: false,
|
||||
isKeyUnlocked: false,
|
||||
inbox: [],
|
||||
@@ -55,9 +58,18 @@ export function useMessaging() {
|
||||
const publicKeyRef = useRef<Uint8Array | null>(null);
|
||||
const pollIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// Check if messaging pallet exists on chain
|
||||
const checkPalletAvailability = useCallback((): boolean => {
|
||||
if (!peopleApi || !isPeopleReady) return false;
|
||||
const available = isPalletAvailable(peopleApi);
|
||||
setState(prev => ({ ...prev, palletReady: available }));
|
||||
return available;
|
||||
}, [peopleApi, isPeopleReady]);
|
||||
|
||||
// Check if user has a registered encryption key on-chain
|
||||
const checkKeyRegistration = useCallback(async () => {
|
||||
if (!peopleApi || !isPeopleReady || !selectedAccount) return;
|
||||
if (!isPalletAvailable(peopleApi)) return;
|
||||
|
||||
try {
|
||||
const key = await getEncryptionKey(peopleApi, selectedAccount.address);
|
||||
@@ -74,19 +86,27 @@ export function useMessaging() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check pallet BEFORE asking for signature
|
||||
if (!isPalletAvailable(peopleApi)) {
|
||||
toast.error('Messaging pallet is not yet available on this chain. Runtime upgrade required.');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(prev => ({ ...prev, registering: true }));
|
||||
|
||||
try {
|
||||
// 1. Sign deterministic message to derive keys
|
||||
// 1. Check if key is already registered on-chain (no signing needed)
|
||||
const existingKey = await getEncryptionKey(peopleApi, selectedAccount.address);
|
||||
|
||||
// 2. Now sign to derive keys
|
||||
const signature = await signMessage('PEZMessage:v1');
|
||||
const { publicKey, privateKey } = deriveKeypair(signature);
|
||||
|
||||
// 2. Store keys in memory
|
||||
// 3. Store keys in memory
|
||||
privateKeyRef.current = privateKey;
|
||||
publicKeyRef.current = publicKey;
|
||||
|
||||
// 3. Check if key is already registered on-chain
|
||||
const existingKey = await getEncryptionKey(peopleApi, selectedAccount.address);
|
||||
// 4. If key already matches, just unlock
|
||||
const alreadyRegistered = existingKey !== null &&
|
||||
existingKey.length === publicKey.length &&
|
||||
existingKey.every((b, i) => b === publicKey[i]);
|
||||
@@ -102,7 +122,7 @@ export function useMessaging() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. Register key on-chain
|
||||
// 5. Register key on-chain
|
||||
const tx = buildRegisterKeyTx(peopleApi, publicKey);
|
||||
const injector = await getSigner(selectedAccount.address, walletSource, peopleApi);
|
||||
|
||||
@@ -163,6 +183,7 @@ export function useMessaging() {
|
||||
// Refresh inbox from chain
|
||||
const refreshInbox = useCallback(async () => {
|
||||
if (!peopleApi || !isPeopleReady || !selectedAccount) return;
|
||||
if (!isPalletAvailable(peopleApi)) return;
|
||||
|
||||
try {
|
||||
const era = await getCurrentEra(peopleApi);
|
||||
@@ -214,6 +235,10 @@ export function useMessaging() {
|
||||
toast.error('Wallet not connected');
|
||||
return;
|
||||
}
|
||||
if (!isPalletAvailable(peopleApi)) {
|
||||
toast.error('Messaging pallet is not available on this chain');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(prev => ({ ...prev, sending: true }));
|
||||
|
||||
@@ -267,6 +292,7 @@ export function useMessaging() {
|
||||
// Acknowledge messages (optional, feeless)
|
||||
const acknowledge = useCallback(async () => {
|
||||
if (!peopleApi || !selectedAccount) return;
|
||||
if (!isPalletAvailable(peopleApi)) return;
|
||||
|
||||
try {
|
||||
const tx = buildAcknowledgeTx(peopleApi);
|
||||
@@ -284,14 +310,21 @@ export function useMessaging() {
|
||||
setState(prev => ({ ...prev, loading: true }));
|
||||
|
||||
const init = async () => {
|
||||
await checkKeyRegistration();
|
||||
await refreshInbox();
|
||||
const available = checkPalletAvailability();
|
||||
if (available) {
|
||||
await checkKeyRegistration();
|
||||
await refreshInbox();
|
||||
}
|
||||
setState(prev => ({ ...prev, loading: false }));
|
||||
};
|
||||
init();
|
||||
|
||||
// Poll every 12 seconds (1 block interval)
|
||||
pollIntervalRef.current = setInterval(refreshInbox, 12000);
|
||||
// Poll every 12 seconds (1 block interval) - only if pallet exists
|
||||
pollIntervalRef.current = setInterval(() => {
|
||||
if (isPalletAvailable(peopleApi)) {
|
||||
refreshInbox();
|
||||
}
|
||||
}, 12000);
|
||||
|
||||
return () => {
|
||||
if (pollIntervalRef.current) {
|
||||
@@ -299,7 +332,7 @@ export function useMessaging() {
|
||||
pollIntervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [peopleApi, isPeopleReady, selectedAccount, checkKeyRegistration, refreshInbox]);
|
||||
}, [peopleApi, isPeopleReady, selectedAccount, checkPalletAvailability, checkKeyRegistration, refreshInbox]);
|
||||
|
||||
// Clear private key when account changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -9,6 +9,19 @@ export interface EncryptedMessage {
|
||||
ciphertext: Uint8Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the messaging pallet exists in the runtime metadata.
|
||||
* Must be called before any other chain function.
|
||||
*/
|
||||
export function isPalletAvailable(api: ApiPromise): boolean {
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return !!(api.query as any).messaging;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Storage queries ---
|
||||
|
||||
export async function getEncryptionKey(
|
||||
@@ -16,7 +29,9 @@ export async function getEncryptionKey(
|
||||
address: string
|
||||
): Promise<Uint8Array | null> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await (api.query as any).messaging.encryptionKeys(address);
|
||||
const messaging = (api.query as any).messaging;
|
||||
if (!messaging?.encryptionKeys) return null;
|
||||
const result = await messaging.encryptionKeys(address);
|
||||
if (result.isNone || result.isEmpty) return null;
|
||||
const hex = result.unwrap().toHex();
|
||||
return hexToBytes(hex);
|
||||
@@ -24,7 +39,9 @@ export async function getEncryptionKey(
|
||||
|
||||
export async function getCurrentEra(api: ApiPromise): Promise<number> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const era = await (api.query as any).messaging.currentEra();
|
||||
const messaging = (api.query as any).messaging;
|
||||
if (!messaging?.currentEra) return 0;
|
||||
const era = await messaging.currentEra();
|
||||
return era.toNumber();
|
||||
}
|
||||
|
||||
@@ -34,7 +51,9 @@ export async function getInbox(
|
||||
address: string
|
||||
): Promise<EncryptedMessage[]> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result = await (api.query as any).messaging.inbox([era, address]);
|
||||
const messaging = (api.query as any).messaging;
|
||||
if (!messaging?.inbox) return [];
|
||||
const result = await messaging.inbox([era, address]);
|
||||
if (result.isEmpty || result.length === 0) return [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -55,7 +74,9 @@ export async function getSendCount(
|
||||
address: string
|
||||
): Promise<number> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const count = await (api.query as any).messaging.sendCount([era, address]);
|
||||
const messaging = (api.query as any).messaging;
|
||||
if (!messaging?.sendCount) return 0;
|
||||
const count = await messaging.sendCount([era, address]);
|
||||
return count.toNumber();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { usePezkuwi } from '@/contexts/PezkuwiContext';
|
||||
import { useMessaging } from '@/hooks/useMessaging';
|
||||
@@ -14,12 +15,15 @@ import {
|
||||
Loader2,
|
||||
Wallet,
|
||||
Inbox,
|
||||
AlertTriangle,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Messaging() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { peopleApi, isPeopleReady, selectedAccount } = usePezkuwi();
|
||||
const {
|
||||
palletReady,
|
||||
isKeyRegistered,
|
||||
isKeyUnlocked,
|
||||
decryptedMessages,
|
||||
@@ -130,7 +134,20 @@ export default function Messaging() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pallet not available banner */}
|
||||
{!loading && !palletReady && (
|
||||
<Card className="border-orange-500/30 bg-orange-500/5 mb-4">
|
||||
<CardContent className="flex items-center gap-3 p-4">
|
||||
<AlertTriangle className="w-5 h-5 text-orange-400 flex-shrink-0" />
|
||||
<p className="text-sm text-orange-300">
|
||||
{t('messaging.palletNotReady', 'Messaging pallet is not yet available on People Chain. A runtime upgrade is required.')}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Key Setup Banner */}
|
||||
{palletReady && (
|
||||
<div className="mb-4">
|
||||
<KeySetup
|
||||
isKeyRegistered={isKeyRegistered}
|
||||
@@ -140,6 +157,7 @@ export default function Messaging() {
|
||||
onUnlockKey={unlockKey}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Era / Stats bar */}
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500 mb-4 px-1">
|
||||
@@ -196,6 +214,16 @@ export default function Messaging() {
|
||||
onSend={sendMessage}
|
||||
sending={sending}
|
||||
/>
|
||||
|
||||
{/* Back to Home */}
|
||||
<div className="fixed bottom-4 right-4 sm:bottom-8 sm:right-8 z-50">
|
||||
<button
|
||||
onClick={() => navigate('/')}
|
||||
className="bg-green-600 hover:bg-green-700 text-white px-3 py-2 sm:px-6 sm:py-3 text-sm sm:text-base rounded-full shadow-lg flex items-center gap-2 transition-all"
|
||||
>
|
||||
{`← ${t('common.backToHome')}`}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user