diff --git a/web/src/hooks/useMessaging.ts b/web/src/hooks/useMessaging.ts index ebb29be5..cb1f8b62 100644 --- a/web/src/hooks/useMessaging.ts +++ b/web/src/hooks/useMessaging.ts @@ -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({ + palletReady: false, isKeyRegistered: false, isKeyUnlocked: false, inbox: [], @@ -55,9 +58,18 @@ export function useMessaging() { const publicKeyRef = useRef(null); const pollIntervalRef = useRef | 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(() => { diff --git a/web/src/lib/messaging/chain.ts b/web/src/lib/messaging/chain.ts index f20ef926..005a228f 100644 --- a/web/src/lib/messaging/chain.ts +++ b/web/src/lib/messaging/chain.ts @@ -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 { // 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 { // 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 { // 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 { // 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(); } diff --git a/web/src/pages/Messaging.tsx b/web/src/pages/Messaging.tsx index 3d05952a..1f20aee5 100644 --- a/web/src/pages/Messaging.tsx +++ b/web/src/pages/Messaging.tsx @@ -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() { + {/* Pallet not available banner */} + {!loading && !palletReady && ( + + + +

+ {t('messaging.palletNotReady', 'Messaging pallet is not yet available on People Chain. A runtime upgrade is required.')} +

+
+
+ )} + {/* Key Setup Banner */} + {palletReady && (
+ )} {/* Era / Stats bar */}
@@ -196,6 +214,16 @@ export default function Messaging() { onSend={sendMessage} sending={sending} /> + + {/* Back to Home */} +
+ +
); }