mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-22 02:07:55 +00:00
fix: refresh inbox after key unlock + query previous era for message visibility
- Call refreshInbox() immediately after setupKey/unlockKey so messages decrypt instantly instead of waiting for 12s polling interval - Query both current and previous era to prevent message loss at era boundaries - Add toJSON fallback for robust field parsing in getInbox - Improve debug logging with era, address, and field diagnostics
This commit is contained in:
@@ -79,6 +79,73 @@ export function useMessaging() {
|
|||||||
}
|
}
|
||||||
}, [peopleApi, isPeopleReady, selectedAccount]);
|
}, [peopleApi, isPeopleReady, selectedAccount]);
|
||||||
|
|
||||||
|
// Decrypt a list of EncryptedMessages using the current private key
|
||||||
|
const decryptInbox = useCallback((inbox: EncryptedMessage[]): DecryptedMessage[] => {
|
||||||
|
if (privateKeyRef.current) {
|
||||||
|
return inbox.map(msg => {
|
||||||
|
try {
|
||||||
|
const plaintext = decryptMessage(
|
||||||
|
privateKeyRef.current!,
|
||||||
|
msg.ephemeralPublicKey,
|
||||||
|
msg.nonce,
|
||||||
|
msg.ciphertext
|
||||||
|
);
|
||||||
|
return { sender: msg.sender, blockNumber: msg.blockNumber, plaintext, raw: msg };
|
||||||
|
} catch (err) {
|
||||||
|
const errText = err instanceof Error ? err.message : String(err);
|
||||||
|
const toHx = (u: Uint8Array) => Array.from(u.slice(0, 4)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
const dbg = `e:${toHx(msg.ephemeralPublicKey)} n:${toHx(msg.nonce)} c:${toHx(msg.ciphertext)} L:${msg.ephemeralPublicKey?.length}/${msg.nonce?.length}/${msg.ciphertext?.length}`;
|
||||||
|
return { sender: msg.sender, blockNumber: msg.blockNumber, plaintext: `[${errText}] ${dbg}`, raw: msg };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return inbox.map(msg => ({
|
||||||
|
sender: msg.sender,
|
||||||
|
blockNumber: msg.blockNumber,
|
||||||
|
plaintext: null,
|
||||||
|
raw: msg,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Refresh inbox from chain (queries current + previous era)
|
||||||
|
const refreshInbox = useCallback(async () => {
|
||||||
|
if (!peopleApi || !isPeopleReady || !selectedAccount) return;
|
||||||
|
if (!isPalletAvailable(peopleApi)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const era = await getCurrentEra(peopleApi);
|
||||||
|
const addr = selectedAccount.address;
|
||||||
|
|
||||||
|
// Query current era + previous era (messages purge at era boundary)
|
||||||
|
const queries: Promise<EncryptedMessage[]>[] = [
|
||||||
|
getInbox(peopleApi, era, addr),
|
||||||
|
];
|
||||||
|
if (era > 0) {
|
||||||
|
queries.push(getInbox(peopleApi, era - 1, addr));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [currentInbox, prevInbox = []] = await Promise.all(queries);
|
||||||
|
const sendCount = await getSendCount(peopleApi, era, addr);
|
||||||
|
|
||||||
|
// Merge: previous era messages first, then current era
|
||||||
|
const allMessages = [...prevInbox, ...currentInbox];
|
||||||
|
|
||||||
|
console.log(`[PEZMessage] refreshInbox era=${era} addr=${addr.slice(0, 8)}… current=${currentInbox.length} prev=${prevInbox.length} total=${allMessages.length} keyUnlocked=${!!privateKeyRef.current}`);
|
||||||
|
|
||||||
|
const decrypted = decryptInbox(allMessages);
|
||||||
|
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
era,
|
||||||
|
inbox: allMessages,
|
||||||
|
sendCount,
|
||||||
|
decryptedMessages: decrypted,
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[PEZMessage] Failed to refresh inbox:', err);
|
||||||
|
}
|
||||||
|
}, [peopleApi, isPeopleReady, selectedAccount, decryptInbox]);
|
||||||
|
|
||||||
// Derive encryption keys from wallet signature and register on-chain
|
// Derive encryption keys from wallet signature and register on-chain
|
||||||
const setupKey = useCallback(async () => {
|
const setupKey = useCallback(async () => {
|
||||||
if (!peopleApi || !isPeopleReady || !selectedAccount) {
|
if (!peopleApi || !isPeopleReady || !selectedAccount) {
|
||||||
@@ -119,6 +186,8 @@ export function useMessaging() {
|
|||||||
registering: false,
|
registering: false,
|
||||||
}));
|
}));
|
||||||
toast.success('Encryption key unlocked');
|
toast.success('Encryption key unlocked');
|
||||||
|
// Refresh inbox now that we have the private key for decryption
|
||||||
|
refreshInbox();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,12 +224,13 @@ export function useMessaging() {
|
|||||||
registering: false,
|
registering: false,
|
||||||
}));
|
}));
|
||||||
toast.success('Encryption key registered');
|
toast.success('Encryption key registered');
|
||||||
|
refreshInbox();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setState(prev => ({ ...prev, registering: false }));
|
setState(prev => ({ ...prev, registering: false }));
|
||||||
const msg = err instanceof Error ? err.message : 'Failed to setup key';
|
const msg = err instanceof Error ? err.message : 'Failed to setup key';
|
||||||
toast.error(msg);
|
toast.error(msg);
|
||||||
}
|
}
|
||||||
}, [peopleApi, isPeopleReady, selectedAccount, walletSource, signMessage]);
|
}, [peopleApi, isPeopleReady, selectedAccount, walletSource, signMessage, refreshInbox]);
|
||||||
|
|
||||||
// Unlock existing key (re-derive from signature without registering)
|
// Unlock existing key (re-derive from signature without registering)
|
||||||
const unlockKey = useCallback(async () => {
|
const unlockKey = useCallback(async () => {
|
||||||
@@ -184,64 +254,13 @@ export function useMessaging() {
|
|||||||
publicKeyRef.current = publicKey;
|
publicKeyRef.current = publicKey;
|
||||||
setState(prev => ({ ...prev, isKeyUnlocked: true, registering: false }));
|
setState(prev => ({ ...prev, isKeyUnlocked: true, registering: false }));
|
||||||
toast.success('Encryption key unlocked');
|
toast.success('Encryption key unlocked');
|
||||||
|
// Refresh inbox now that we have the private key for decryption
|
||||||
|
refreshInbox();
|
||||||
} catch {
|
} catch {
|
||||||
setState(prev => ({ ...prev, registering: false }));
|
setState(prev => ({ ...prev, registering: false }));
|
||||||
toast.error('Failed to unlock key');
|
toast.error('Failed to unlock key');
|
||||||
}
|
}
|
||||||
}, [peopleApi, selectedAccount, signMessage]);
|
}, [peopleApi, selectedAccount, signMessage, refreshInbox]);
|
||||||
|
|
||||||
// Refresh inbox from chain
|
|
||||||
const refreshInbox = useCallback(async () => {
|
|
||||||
if (!peopleApi || !isPeopleReady || !selectedAccount) return;
|
|
||||||
if (!isPalletAvailable(peopleApi)) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const era = await getCurrentEra(peopleApi);
|
|
||||||
const [inbox, sendCount] = await Promise.all([
|
|
||||||
getInbox(peopleApi, era, selectedAccount.address),
|
|
||||||
getSendCount(peopleApi, era, selectedAccount.address),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Auto-decrypt if private key is available
|
|
||||||
let decrypted: DecryptedMessage[] = [];
|
|
||||||
if (privateKeyRef.current) {
|
|
||||||
decrypted = inbox.map(msg => {
|
|
||||||
try {
|
|
||||||
const plaintext = decryptMessage(
|
|
||||||
privateKeyRef.current!,
|
|
||||||
msg.ephemeralPublicKey,
|
|
||||||
msg.nonce,
|
|
||||||
msg.ciphertext
|
|
||||||
);
|
|
||||||
return { sender: msg.sender, blockNumber: msg.blockNumber, plaintext, raw: msg };
|
|
||||||
} catch (err) {
|
|
||||||
const errText = err instanceof Error ? err.message : String(err);
|
|
||||||
// Show first 8 hex chars of each field for comparison with raw SCALE
|
|
||||||
const toHx = (u: Uint8Array) => Array.from(u.slice(0, 4)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
||||||
const dbg = `e:${toHx(msg.ephemeralPublicKey)} n:${toHx(msg.nonce)} c:${toHx(msg.ciphertext)} L:${msg.ephemeralPublicKey?.length}/${msg.nonce?.length}/${msg.ciphertext?.length}`;
|
|
||||||
return { sender: msg.sender, blockNumber: msg.blockNumber, plaintext: `[${errText}] ${dbg}`, raw: msg };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
decrypted = inbox.map(msg => ({
|
|
||||||
sender: msg.sender,
|
|
||||||
blockNumber: msg.blockNumber,
|
|
||||||
plaintext: null,
|
|
||||||
raw: msg,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
setState(prev => ({
|
|
||||||
...prev,
|
|
||||||
era,
|
|
||||||
inbox,
|
|
||||||
sendCount,
|
|
||||||
decryptedMessages: decrypted,
|
|
||||||
}));
|
|
||||||
} catch (err) {
|
|
||||||
if (import.meta.env.DEV) console.error('Failed to refresh inbox:', err);
|
|
||||||
}
|
|
||||||
}, [peopleApi, isPeopleReady, selectedAccount]);
|
|
||||||
|
|
||||||
// Send an encrypted message
|
// Send an encrypted message
|
||||||
const sendEncryptedMessage = useCallback(async (recipient: string, text: string) => {
|
const sendEncryptedMessage = useCallback(async (recipient: string, text: string) => {
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ export async function getEncryptionKey(
|
|||||||
if (!messaging?.encryptionKeys) return null;
|
if (!messaging?.encryptionKeys) return null;
|
||||||
const result = await messaging.encryptionKeys(address);
|
const result = await messaging.encryptionKeys(address);
|
||||||
if (result.isNone || result.isEmpty) return null;
|
if (result.isNone || result.isEmpty) return null;
|
||||||
const hex = result.unwrap().toHex();
|
// Handle both Option<[u8;32]> (needs unwrap) and direct [u8;32]
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const inner = typeof (result as any).unwrap === 'function' ? (result as any).unwrap() : result;
|
||||||
|
const hex = inner.toHex();
|
||||||
|
console.log(`[PEZMessage] getEncryptionKey(${address.slice(0, 8)}…) = ${hex.slice(0, 18)}…`);
|
||||||
return hexToBytes(hex);
|
return hexToBytes(hex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,30 +58,63 @@ export async function getInbox(
|
|||||||
const messaging = (api.query as any).messaging;
|
const messaging = (api.query as any).messaging;
|
||||||
if (!messaging?.inbox) return [];
|
if (!messaging?.inbox) return [];
|
||||||
const result = await messaging.inbox(era, address);
|
const result = await messaging.inbox(era, address);
|
||||||
console.log('[PEZMessage] raw inbox result:', JSON.stringify(result.toHuman?.() ?? result));
|
|
||||||
|
// Debug: log raw result type and content
|
||||||
|
const human = result.toHuman?.() ?? result;
|
||||||
|
console.log(`[PEZMessage] getInbox(era=${era}, addr=${address.slice(0, 8)}…) isEmpty=${result.isEmpty} length=${result.length} raw:`, JSON.stringify(human));
|
||||||
|
|
||||||
if (result.isEmpty || result.length === 0) return [];
|
if (result.isEmpty || result.length === 0) return [];
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const first = result[0];
|
const first = result[0];
|
||||||
if (first) {
|
if (first) {
|
||||||
const keys = Object.getOwnPropertyNames(first).filter(k => !k.startsWith('_'));
|
// Log all available field access patterns
|
||||||
|
const ownKeys = Object.getOwnPropertyNames(first).filter(k => !k.startsWith('_'));
|
||||||
const json = first.toJSON?.() ?? {};
|
const json = first.toJSON?.() ?? {};
|
||||||
const jsonKeys = Object.keys(json);
|
const jsonKeys = Object.keys(json);
|
||||||
console.log('[PEZMessage] field names:', keys, 'json keys:', jsonKeys, 'json:', JSON.stringify(json));
|
console.log('[PEZMessage] struct field names (own):', ownKeys, '(json):', jsonKeys);
|
||||||
|
console.log('[PEZMessage] first message json:', JSON.stringify(json));
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return result.map((msg: Record<string, any>) => {
|
return result.map((msg: Record<string, any>, idx: number) => {
|
||||||
// Try multiple field name patterns
|
// Try codec accessors first (camelCase), then snake_case, then toJSON fallback
|
||||||
const eph = msg.ephemeralPublicKey ?? msg.ephemeral_public_key ?? msg.ephemeralPubKey ?? msg.ephemeral_pub_key;
|
let eph = msg.ephemeralPublicKey ?? msg.ephemeral_public_key;
|
||||||
const blk = msg.blockNumber ?? msg.block_number ?? msg.blockNum;
|
let blk = msg.blockNumber ?? msg.block_number;
|
||||||
const ct = msg.ciphertext ?? msg.cipher_text;
|
let ct = msg.ciphertext ?? msg.cipher_text;
|
||||||
|
const nonce = msg.nonce;
|
||||||
|
const sender = msg.sender;
|
||||||
|
|
||||||
|
// Fallback: use toJSON() if codec accessors returned undefined
|
||||||
|
if (!eph || !nonce || !ct) {
|
||||||
|
const json = msg.toJSON?.() ?? {};
|
||||||
|
console.warn(`[PEZMessage] msg[${idx}] codec accessors incomplete, using toJSON fallback:`, JSON.stringify(json));
|
||||||
|
if (!eph) eph = json.ephemeralPublicKey ?? json.ephemeral_public_key;
|
||||||
|
if (!blk && blk !== 0) blk = json.blockNumber ?? json.block_number;
|
||||||
|
if (!ct) ct = json.ciphertext ?? json.cipher_text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to bytes - handle both codec objects (.toHex()) and raw hex strings
|
||||||
|
const toBytes = (val: unknown, label: string, expectedLen?: number): Uint8Array => {
|
||||||
|
if (!val) {
|
||||||
|
console.error(`[PEZMessage] msg[${idx}].${label} is null/undefined`);
|
||||||
|
return new Uint8Array(expectedLen ?? 0);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const hex = typeof (val as any).toHex === 'function' ? (val as any).toHex() : typeof val === 'string' ? val : '0x';
|
||||||
|
const bytes = hexToBytes(hex);
|
||||||
|
if (expectedLen && bytes.length !== expectedLen) {
|
||||||
|
console.warn(`[PEZMessage] msg[${idx}].${label} expected ${expectedLen} bytes, got ${bytes.length}`);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sender: msg.sender.toString(),
|
sender: sender?.toString?.() ?? '',
|
||||||
blockNumber: blk?.toNumber?.() ?? 0,
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
ephemeralPublicKey: hexToBytes(eph?.toHex?.() ?? '0x'),
|
blockNumber: typeof blk === 'number' ? blk : (blk as any)?.toNumber?.() ?? 0,
|
||||||
nonce: hexToBytes(msg.nonce?.toHex?.() ?? '0x'),
|
ephemeralPublicKey: toBytes(eph, 'ephemeralPublicKey', 32),
|
||||||
ciphertext: hexToBytes(ct?.toHex?.() ?? '0x'),
|
nonce: toBytes(nonce, 'nonce', 24),
|
||||||
|
ciphertext: toBytes(ct, 'ciphertext'),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user