mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-04 05:43:10 +00:00
feat: key delegation
This commit is contained in:
parent
8859b85367
commit
8bfbb41bd8
@ -218,8 +218,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
try {
|
||||
toast({
|
||||
title: "Delegating Key",
|
||||
description: "Generating a browser keypair for quick signing...",
|
||||
title: "Starting Key Delegation",
|
||||
description: "This will let you post, comment, and vote without approving each action for 24 hours.",
|
||||
});
|
||||
|
||||
// Generate a browser keypair
|
||||
@ -236,9 +236,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
expiryTimestamp
|
||||
);
|
||||
|
||||
// Format date for user-friendly display
|
||||
const expiryDate = new Date(expiryTimestamp);
|
||||
const formattedExpiry = expiryDate.toLocaleString();
|
||||
|
||||
toast({
|
||||
title: "Signing Required",
|
||||
description: "Please sign the delegation message with your wallet...",
|
||||
title: "Wallet Signature Required",
|
||||
description: `Please sign with your wallet to authorize a temporary key valid until ${formattedExpiry}. This improves UX by reducing wallet prompts.`,
|
||||
});
|
||||
|
||||
const signature = await phantomWalletRef.current.signMessage(delegationMessage);
|
||||
@ -264,8 +268,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
localStorage.setItem('opchan-user', JSON.stringify(updatedUser));
|
||||
|
||||
toast({
|
||||
title: "Key Delegated",
|
||||
description: `You won't need to sign every action for the next ${expiryHours} hours.`,
|
||||
title: "Key Delegation Successful",
|
||||
description: `You can now interact with the forum without additional wallet approvals until ${formattedExpiry}.`,
|
||||
});
|
||||
|
||||
return true;
|
||||
@ -273,12 +277,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
console.error("Error delegating key:", error);
|
||||
|
||||
let errorMessage = "Failed to delegate key. Please try again.";
|
||||
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
// Provide specific guidance based on error type
|
||||
if (error.message.includes("rejected") || error.message.includes("declined") || error.message.includes("denied")) {
|
||||
errorMessage = "You declined the signature request. Key delegation is optional but improves your experience.";
|
||||
} else if (error.message.includes("timeout")) {
|
||||
errorMessage = "Wallet request timed out. Please try again and approve the signature promptly.";
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Delegation Error",
|
||||
title: "Delegation Failed",
|
||||
description: errorMessage,
|
||||
variant: "destructive",
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { Cell, Post, Comment } from '@/types';
|
||||
import { Cell, Post, Comment, OpchanMessage } from '@/types';
|
||||
import { useToast } from '@/components/ui/use-toast';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import {
|
||||
@ -61,25 +61,30 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
// Transform message cache data to the expected types
|
||||
const updateStateFromCache = () => {
|
||||
// Transform cells
|
||||
// Use the verifyMessage function from messageSigning if available
|
||||
const verifyFn = isAuthenticated && messageSigning ?
|
||||
(message: OpchanMessage) => messageSigning.verifyMessage(message) :
|
||||
undefined;
|
||||
|
||||
// Transform cells with verification
|
||||
setCells(
|
||||
Object.values(messageManager.messageCache.cells).map(cell =>
|
||||
transformCell(cell)
|
||||
)
|
||||
Object.values(messageManager.messageCache.cells)
|
||||
.map(cell => transformCell(cell, verifyFn))
|
||||
.filter(cell => cell !== null) as Cell[]
|
||||
);
|
||||
|
||||
// Transform posts
|
||||
// Transform posts with verification
|
||||
setPosts(
|
||||
Object.values(messageManager.messageCache.posts).map(post =>
|
||||
transformPost(post)
|
||||
)
|
||||
Object.values(messageManager.messageCache.posts)
|
||||
.map(post => transformPost(post, verifyFn))
|
||||
.filter(post => post !== null) as Post[]
|
||||
);
|
||||
|
||||
// Transform comments
|
||||
// Transform comments with verification
|
||||
setComments(
|
||||
Object.values(messageManager.messageCache.comments).map(comment =>
|
||||
transformComment(comment)
|
||||
)
|
||||
Object.values(messageManager.messageCache.comments)
|
||||
.map(comment => transformComment(comment, verifyFn))
|
||||
.filter(comment => comment !== null) as Comment[]
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -33,11 +33,24 @@ async function signAndSendMessage<T extends PostMessage | CommentMessage | VoteM
|
||||
signedMessage = await messageSigning.signMessage(message);
|
||||
|
||||
if (!signedMessage) {
|
||||
toast({
|
||||
title: "Key Delegation Required",
|
||||
description: "Please delegate a signing key for better UX.",
|
||||
variant: "destructive",
|
||||
});
|
||||
// Check if delegation exists but is expired
|
||||
const isDelegationExpired = messageSigning['keyDelegation'] &&
|
||||
!messageSigning['keyDelegation'].isDelegationValid() &&
|
||||
messageSigning['keyDelegation'].retrieveDelegation();
|
||||
|
||||
if (isDelegationExpired) {
|
||||
toast({
|
||||
title: "Key Delegation Expired",
|
||||
description: "Your signing key has expired. Please re-delegate your key through the profile menu.",
|
||||
variant: "destructive",
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: "Key Delegation Required",
|
||||
description: "Please delegate a signing key from your profile menu to post without wallet approval for each action.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
@ -48,9 +61,20 @@ async function signAndSendMessage<T extends PostMessage | CommentMessage | VoteM
|
||||
return signedMessage;
|
||||
} catch (error) {
|
||||
console.error("Error signing and sending message:", error);
|
||||
|
||||
let errorMessage = "Failed to sign and send message. Please try again.";
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("timeout") || error.message.includes("network")) {
|
||||
errorMessage = "Network issue detected. Please check your connection and try again.";
|
||||
} else if (error.message.includes("rejected") || error.message.includes("denied")) {
|
||||
errorMessage = "Wallet signature request was rejected. Please approve signing to continue.";
|
||||
}
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Message Error",
|
||||
description: "Failed to sign and send message. Please try again.",
|
||||
description: errorMessage,
|
||||
variant: "destructive",
|
||||
});
|
||||
return null;
|
||||
|
||||
@ -1,26 +1,55 @@
|
||||
import { Cell, Post, Comment } from '@/types';
|
||||
import { CellMessage, CommentMessage, PostMessage } from '@/lib/waku/types';
|
||||
import { Cell, Post, Comment, OpchanMessage } from '@/types';
|
||||
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from '@/lib/waku/types';
|
||||
import messageManager from '@/lib/waku';
|
||||
|
||||
// Type for the verification function
|
||||
type VerifyFunction = (message: OpchanMessage) => boolean;
|
||||
|
||||
// Helper function to transform CellMessage to Cell
|
||||
export const transformCell = (cellMessage: CellMessage): Cell => {
|
||||
export const transformCell = (
|
||||
cellMessage: CellMessage,
|
||||
verifyMessage?: VerifyFunction
|
||||
): Cell | null => {
|
||||
// Verify the message if a verification function is provided
|
||||
if (verifyMessage && !verifyMessage(cellMessage)) {
|
||||
console.warn(`Cell message ${cellMessage.id} failed verification`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: cellMessage.id,
|
||||
name: cellMessage.name,
|
||||
description: cellMessage.description,
|
||||
icon: cellMessage.icon
|
||||
icon: cellMessage.icon,
|
||||
// Include signature information for future verification if needed
|
||||
signature: cellMessage.signature,
|
||||
browserPubKey: cellMessage.browserPubKey
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to transform PostMessage to Post with vote aggregation
|
||||
export const transformPost = (postMessage: PostMessage): Post => {
|
||||
export const transformPost = (
|
||||
postMessage: PostMessage,
|
||||
verifyMessage?: VerifyFunction
|
||||
): Post | null => {
|
||||
// Verify the message if a verification function is provided
|
||||
if (verifyMessage && !verifyMessage(postMessage)) {
|
||||
console.warn(`Post message ${postMessage.id} failed verification`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find all votes related to this post
|
||||
const votes = Object.values(messageManager.messageCache.votes).filter(
|
||||
vote => vote.targetId === postMessage.id
|
||||
);
|
||||
|
||||
const upvotes = votes.filter(vote => vote.value === 1);
|
||||
const downvotes = votes.filter(vote => vote.value === -1);
|
||||
// Only include verified votes if verification function is provided
|
||||
const filteredVotes = verifyMessage
|
||||
? votes.filter(vote => verifyMessage(vote))
|
||||
: votes;
|
||||
|
||||
const upvotes = filteredVotes.filter(vote => vote.value === 1);
|
||||
const downvotes = filteredVotes.filter(vote => vote.value === -1);
|
||||
|
||||
return {
|
||||
id: postMessage.id,
|
||||
@ -30,19 +59,36 @@ export const transformPost = (postMessage: PostMessage): Post => {
|
||||
content: postMessage.content,
|
||||
timestamp: postMessage.timestamp,
|
||||
upvotes: upvotes,
|
||||
downvotes: downvotes
|
||||
downvotes: downvotes,
|
||||
// Include signature information for future verification if needed
|
||||
signature: postMessage.signature,
|
||||
browserPubKey: postMessage.browserPubKey
|
||||
};
|
||||
};
|
||||
|
||||
// Helper function to transform CommentMessage to Comment with vote aggregation
|
||||
export const transformComment = (commentMessage: CommentMessage): Comment => {
|
||||
export const transformComment = (
|
||||
commentMessage: CommentMessage,
|
||||
verifyMessage?: VerifyFunction
|
||||
): Comment | null => {
|
||||
// Verify the message if a verification function is provided
|
||||
if (verifyMessage && !verifyMessage(commentMessage)) {
|
||||
console.warn(`Comment message ${commentMessage.id} failed verification`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Find all votes related to this comment
|
||||
const votes = Object.values(messageManager.messageCache.votes).filter(
|
||||
vote => vote.targetId === commentMessage.id
|
||||
);
|
||||
|
||||
const upvotes = votes.filter(vote => vote.value === 1);
|
||||
const downvotes = votes.filter(vote => vote.value === -1);
|
||||
// Only include verified votes if verification function is provided
|
||||
const filteredVotes = verifyMessage
|
||||
? votes.filter(vote => verifyMessage(vote))
|
||||
: votes;
|
||||
|
||||
const upvotes = filteredVotes.filter(vote => vote.value === 1);
|
||||
const downvotes = filteredVotes.filter(vote => vote.value === -1);
|
||||
|
||||
return {
|
||||
id: commentMessage.id,
|
||||
@ -51,20 +97,43 @@ export const transformComment = (commentMessage: CommentMessage): Comment => {
|
||||
content: commentMessage.content,
|
||||
timestamp: commentMessage.timestamp,
|
||||
upvotes: upvotes,
|
||||
downvotes: downvotes
|
||||
downvotes: downvotes,
|
||||
// Include signature information for future verification if needed
|
||||
signature: commentMessage.signature,
|
||||
browserPubKey: commentMessage.browserPubKey
|
||||
};
|
||||
};
|
||||
|
||||
// Function to update UI state from message cache
|
||||
export const getDataFromCache = () => {
|
||||
// Transform cells
|
||||
const cells = Object.values(messageManager.messageCache.cells).map(transformCell);
|
||||
// Helper function to transform VoteMessage (new)
|
||||
export const transformVote = (
|
||||
voteMessage: VoteMessage,
|
||||
verifyMessage?: VerifyFunction
|
||||
): VoteMessage | null => {
|
||||
// Verify the message if a verification function is provided
|
||||
if (verifyMessage && !verifyMessage(voteMessage)) {
|
||||
console.warn(`Vote message ${voteMessage.id} failed verification`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Transform posts
|
||||
const posts = Object.values(messageManager.messageCache.posts).map(transformPost);
|
||||
return voteMessage;
|
||||
};
|
||||
|
||||
// Transform comments
|
||||
const comments = Object.values(messageManager.messageCache.comments).map(transformComment);
|
||||
// Function to update UI state from message cache with verification
|
||||
export const getDataFromCache = (verifyMessage?: VerifyFunction) => {
|
||||
// Transform cells with verification
|
||||
const cells = Object.values(messageManager.messageCache.cells)
|
||||
.map(cell => transformCell(cell, verifyMessage))
|
||||
.filter(cell => cell !== null) as Cell[];
|
||||
|
||||
// Transform posts with verification
|
||||
const posts = Object.values(messageManager.messageCache.posts)
|
||||
.map(post => transformPost(post, verifyMessage))
|
||||
.filter(post => post !== null) as Post[];
|
||||
|
||||
// Transform comments with verification
|
||||
const comments = Object.values(messageManager.messageCache.comments)
|
||||
.map(comment => transformComment(comment, verifyMessage))
|
||||
.filter(comment => comment !== null) as Comment[];
|
||||
|
||||
return { cells, posts, comments };
|
||||
};
|
||||
@ -1,6 +1,8 @@
|
||||
import { OpchanMessage } from '@/types';
|
||||
import { KeyDelegation } from './key-delegation';
|
||||
|
||||
// Maximum age of a message in milliseconds (24 hours)
|
||||
const MAX_MESSAGE_AGE = 24 * 60 * 60 * 1000;
|
||||
|
||||
export class MessageSigning {
|
||||
private keyDelegation: KeyDelegation;
|
||||
@ -9,7 +11,7 @@ export class MessageSigning {
|
||||
this.keyDelegation = keyDelegation;
|
||||
}
|
||||
|
||||
signMessage<T extends OpchanMessage>(message: T): T | null {
|
||||
signMessage<T extends OpchanMessage>(message: T): T | null {
|
||||
if (!this.keyDelegation.isDelegationValid()) {
|
||||
console.error('No valid key delegation found. Cannot sign message.');
|
||||
return null;
|
||||
@ -35,21 +37,48 @@ export class MessageSigning {
|
||||
}
|
||||
|
||||
verifyMessage(message: OpchanMessage): boolean {
|
||||
// Check for required signature fields
|
||||
if (!message.signature || !message.browserPubKey) {
|
||||
console.warn('Message is missing signature information');
|
||||
console.warn('Message is missing signature information', message.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if message is too old (anti-replay protection)
|
||||
if (this.isMessageTooOld(message)) {
|
||||
console.warn(`Message ${message.id} is too old (timestamp: ${message.timestamp})`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reconstruct the original signed content
|
||||
const signedContent = JSON.stringify({
|
||||
...message,
|
||||
signature: undefined,
|
||||
browserPubKey: undefined
|
||||
});
|
||||
|
||||
return this.keyDelegation.verifySignature(
|
||||
// Verify the signature
|
||||
const isValid = this.keyDelegation.verifySignature(
|
||||
signedContent,
|
||||
message.signature,
|
||||
message.browserPubKey
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
console.warn(`Invalid signature for message ${message.id}`);
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a message's timestamp is older than the maximum allowed age
|
||||
*/
|
||||
private isMessageTooOld(message: OpchanMessage): boolean {
|
||||
if (!message.timestamp) return true;
|
||||
|
||||
const currentTime = Date.now();
|
||||
const messageAge = currentTime - message.timestamp;
|
||||
|
||||
return messageAge > MAX_MESSAGE_AGE;
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,8 @@ export interface Cell {
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
signature?: string; // Message signature
|
||||
browserPubKey?: string; // Public key that signed the message
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user