From 72f3e272577d8b4f5a1a1e0c35d9e0b6cc967697 Mon Sep 17 00:00:00 2001 From: shitikyan Date: Wed, 18 Mar 2026 18:13:03 +0400 Subject: [PATCH 1/6] feat: add L2PS messaging client and protocol types - L2PSMessagingPeer: WebSocket client with auto-reconnect, event handlers, request-response pattern - l2ps_types: full protocol types (client/server messages, encryption, storage, errors) - L2PSInstantMessagingPayload: transaction payload type for L2PS-backed messaging - Updated Transaction union type and exports Co-Authored-By: Claude Opus 4.6 (1M context) --- src/instant_messaging/L2PSMessagingPeer.ts | 514 ++++++++++++++++++ src/instant_messaging/index.ts | 13 + src/instant_messaging/l2ps_types.ts | 246 +++++++++ src/types/blockchain/Transaction.ts | 3 +- .../InstantMessagingTransaction.ts | 14 +- .../blockchain/TransactionSubtypes/index.ts | 3 +- src/types/instantMessaging/index.ts | 19 + 7 files changed, 808 insertions(+), 4 deletions(-) create mode 100644 src/instant_messaging/L2PSMessagingPeer.ts create mode 100644 src/instant_messaging/l2ps_types.ts diff --git a/src/instant_messaging/L2PSMessagingPeer.ts b/src/instant_messaging/L2PSMessagingPeer.ts new file mode 100644 index 00000000..1c1eb328 --- /dev/null +++ b/src/instant_messaging/L2PSMessagingPeer.ts @@ -0,0 +1,514 @@ +/** + * L2PSMessagingPeer - WebSocket client for L2PS-backed instant messaging + * + * Connects to the L2PS messaging server (default port 3006) and provides: + * - Registration with ed25519 proof and L2PS network isolation + * - E2E encrypted message sending/receiving + * - Conversation history with pagination + * - Peer discovery within L2PS network + * - Offline message delivery + * - Automatic reconnection with exponential backoff + */ + +import type { + SerializedEncryptedMessage, + ClientMessageType, + ServerMessageType, + RegisteredResponse, + IncomingMessage, + MessageSentResponse, + MessageQueuedResponse, + HistoryResponse, + DiscoverResponse, + PublicKeyResponse, + PeerJoinedNotification, + PeerLeftNotification, + ErrorResponse, + ErrorCode, + StoredMessage, +} from "./l2ps_types" + +// ─── Config & Handler Types ────────────────────────────────────── + +export interface L2PSMessagingConfig { + /** WebSocket URL of the L2PS messaging server (e.g. "ws://localhost:3006") */ + serverUrl: string + /** Client's ed25519 public key (hex string, 64+ chars) */ + publicKey: string + /** L2PS network UID to join */ + l2psUid: string + /** Function to sign proof strings with ed25519 private key. Returns hex signature. */ + signFn: (message: string) => Promise | string +} + +export type L2PSMessageHandler = (message: IncomingMessage["payload"]) => void +export type L2PSErrorHandler = (error: ErrorResponse["payload"]) => void +export type L2PSPeerHandler = (publicKey: string) => void +export type L2PSConnectionStateHandler = (state: "connected" | "disconnected" | "reconnecting") => void + +// ─── Internal protocol frame ──────────────────────────────────── + +interface OutgoingFrame { + type: ClientMessageType + payload: Record + timestamp: number +} + +interface IncomingFrame { + type: ServerMessageType + payload: any + timestamp: number +} + +// ─── Client Class ──────────────────────────────────────────────── + +export class L2PSMessagingPeer { + private ws: WebSocket | null = null + private config: L2PSMessagingConfig + + // Event handlers + private messageHandlers: Set = new Set() + private errorHandlers: Set = new Set() + private peerJoinedHandlers: Set = new Set() + private peerLeftHandlers: Set = new Set() + private connectionStateHandlers: Set = new Set() + + // Pending request-response waiters + private pendingResponses: Map< + string, + { resolve: (value: any) => void; reject: (error: Error) => void; timer: NodeJS.Timeout } + > = new Map() + + // State + private _isConnected = false + private _isRegistered = false + private onlinePeers: Set = new Set() + private messageQueue: OutgoingFrame[] = [] + + // Reconnection + private reconnectAttempts = 0 + private maxReconnectAttempts = 10 + private baseReconnectDelay = 1000 + private reconnectTimeout: NodeJS.Timeout | null = null + private shouldReconnect = true + + constructor(config: L2PSMessagingConfig) { + this.config = config + } + + // ─── Public Getters ────────────────────────────────────────── + + get isConnected(): boolean { + return this._isConnected + } + + get isRegistered(): boolean { + return this._isRegistered + } + + get peers(): string[] { + return Array.from(this.onlinePeers) + } + + // ─── Connection Lifecycle ──────────────────────────────────── + + /** + * Connect to L2PS messaging server and register + * @returns Registration response with online peers + */ + async connect(): Promise { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error("Connection timeout (10s)")) + }, 10000) + + this.shouldReconnect = true + this.connectWebSocket() + + // Wait for WS open, then register + const checkOpen = setInterval(() => { + if (this._isConnected) { + clearInterval(checkOpen) + this.register() + .then(response => { + clearTimeout(timeout) + resolve(response) + }) + .catch(err => { + clearTimeout(timeout) + reject(err) + }) + } + }, 50) + }) + } + + /** Disconnect from the server */ + disconnect(): void { + this.shouldReconnect = false + if (this.reconnectTimeout) { + clearTimeout(this.reconnectTimeout) + this.reconnectTimeout = null + } + // Reject all pending responses + for (const [key, pending] of this.pendingResponses) { + clearTimeout(pending.timer) + pending.reject(new Error("Disconnected")) + } + this.pendingResponses.clear() + + if (this.ws) { + this.ws.close() + this.ws = null + } + this._isConnected = false + this._isRegistered = false + this.onlinePeers.clear() + this.notifyConnectionState("disconnected") + } + + // ─── Messaging API ─────────────────────────────────────────── + + /** + * Send an encrypted message to a peer + * @param to - Recipient's public key (hex) + * @param encrypted - E2E encrypted message + * @param messageHash - SHA256 hash for dedup + * @returns Delivery confirmation (sent or queued) + */ + async send( + to: string, + encrypted: SerializedEncryptedMessage, + messageHash: string, + ): Promise { + this.ensureRegistered() + + this.sendFrame({ + type: "send", + payload: { to, encrypted, messageHash }, + timestamp: Date.now(), + }) + + // Wait for either message_sent or message_queued + return this.waitForResponse( + `send:${messageHash}`, + ["message_sent", "message_queued"], + 15000, + (frame: IncomingFrame) => + frame.payload?.messageHash === messageHash, + ) + } + + /** + * Get conversation history with a peer + * @param peerKey - Peer's public key + * @param options - Pagination options + * @returns Message history with pagination info + */ + async history( + peerKey: string, + options: { before?: number; limit?: number } = {}, + ): Promise { + this.ensureRegistered() + + const timestamp = Date.now() + const proofString = `history:${peerKey}:${timestamp}` + const proof = await this.config.signFn(proofString) + + this.sendFrame({ + type: "history", + payload: { + peerKey, + before: options.before, + limit: options.limit, + proof, + }, + timestamp, + }) + + return this.waitForResponse( + `history:${peerKey}`, + ["history_response"], + 10000, + ) + } + + /** + * Discover online peers in the L2PS network + * @returns List of online peer public keys + */ + async discover(): Promise { + this.ensureRegistered() + + this.sendFrame({ + type: "discover", + payload: {}, + timestamp: Date.now(), + }) + + const response = await this.waitForResponse( + "discover", + ["discover_response"], + 10000, + ) + + this.onlinePeers = new Set(response.peers) + return response.peers + } + + /** + * Request a peer's public key + * @param targetId - Target peer identifier + * @returns Public key hex string or null if not found + */ + async requestPublicKey(targetId: string): Promise { + this.ensureRegistered() + + this.sendFrame({ + type: "request_public_key", + payload: { targetId }, + timestamp: Date.now(), + }) + + const response = await this.waitForResponse( + `pubkey:${targetId}`, + ["public_key_response"], + 10000, + (frame: IncomingFrame) => frame.payload?.targetId === targetId, + ) + + return response.publicKey + } + + // ─── Event Handlers ────────────────────────────────────────── + + onMessage(handler: L2PSMessageHandler): void { + this.messageHandlers.add(handler) + } + + onError(handler: L2PSErrorHandler): void { + this.errorHandlers.add(handler) + } + + onPeerJoined(handler: L2PSPeerHandler): void { + this.peerJoinedHandlers.add(handler) + } + + onPeerLeft(handler: L2PSPeerHandler): void { + this.peerLeftHandlers.add(handler) + } + + onConnectionStateChange(handler: L2PSConnectionStateHandler): void { + this.connectionStateHandlers.add(handler) + } + + removeMessageHandler(handler: L2PSMessageHandler): void { + this.messageHandlers.delete(handler) + } + + removeErrorHandler(handler: L2PSErrorHandler): void { + this.errorHandlers.delete(handler) + } + + removePeerJoinedHandler(handler: L2PSPeerHandler): void { + this.peerJoinedHandlers.delete(handler) + } + + removePeerLeftHandler(handler: L2PSPeerHandler): void { + this.peerLeftHandlers.delete(handler) + } + + removeConnectionStateHandler(handler: L2PSConnectionStateHandler): void { + this.connectionStateHandlers.delete(handler) + } + + // ─── Private: WebSocket ────────────────────────────────────── + + private connectWebSocket(): void { + if (this.ws) { + this.ws.close() + } + + this.ws = new WebSocket(this.config.serverUrl) + this.notifyConnectionState("reconnecting") + + this.ws.onopen = () => { + this._isConnected = true + this.reconnectAttempts = 0 + this.notifyConnectionState("connected") + this.flushQueue() + } + + this.ws.onclose = () => { + this._isConnected = false + this._isRegistered = false + this.onlinePeers.clear() + this.notifyConnectionState("disconnected") + if (this.shouldReconnect) { + this.attemptReconnect() + } + } + + this.ws.onerror = (event) => { + this.errorHandlers.forEach(h => + h({ code: "INTERNAL_ERROR" as ErrorCode, message: "WebSocket error", details: String(event) }), + ) + } + + this.ws.onmessage = (event) => { + try { + const frame: IncomingFrame = JSON.parse( + typeof event.data === "string" ? event.data : new TextDecoder().decode(event.data), + ) + this.handleFrame(frame) + } catch { + // Ignore unparseable frames + } + } + } + + private attemptReconnect(): void { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + return + } + + const delay = Math.min( + this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts), + 30000, + ) + + this.reconnectTimeout = setTimeout(() => { + this.reconnectAttempts++ + this.connectWebSocket() + }, delay) + } + + // ─── Private: Registration ─────────────────────────────────── + + private async register(): Promise { + const timestamp = Date.now() + const proofString = `register:${this.config.publicKey}:${timestamp}` + const proof = await this.config.signFn(proofString) + + this.sendFrame({ + type: "register", + payload: { + publicKey: this.config.publicKey, + l2psUid: this.config.l2psUid, + proof, + }, + timestamp, + }) + + const response = await this.waitForResponse( + "register", + ["registered"], + 10000, + ) + + this._isRegistered = true + this.onlinePeers = new Set(response.onlinePeers) + return response + } + + // ─── Private: Frame Handling ───────────────────────────────── + + private handleFrame(frame: IncomingFrame): void { + // First, check if any pending response matches + for (const [key, pending] of this.pendingResponses) { + const meta = (pending as any)._meta as PendingMeta | undefined + if (meta && meta.types.includes(frame.type)) { + if (!meta.filterFn || meta.filterFn(frame)) { + clearTimeout(pending.timer) + this.pendingResponses.delete(key) + pending.resolve(frame.payload) + return + } + } + } + + // Then dispatch to event handlers + switch (frame.type) { + case "message": + this.messageHandlers.forEach(h => h(frame.payload)) + break + case "peer_joined": + this.onlinePeers.add(frame.payload.publicKey) + this.peerJoinedHandlers.forEach(h => h(frame.payload.publicKey)) + break + case "peer_left": + this.onlinePeers.delete(frame.payload.publicKey) + this.peerLeftHandlers.forEach(h => h(frame.payload.publicKey)) + break + case "error": + this.errorHandlers.forEach(h => h(frame.payload)) + break + default: + // message_sent, message_queued etc. without a waiter — ignore + break + } + } + + // ─── Private: Request-Response ─────────────────────────────── + + private waitForResponse( + key: string, + expectedTypes: ServerMessageType[], + timeoutMs: number, + filterFn?: (frame: IncomingFrame) => boolean, + ): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pendingResponses.delete(key) + reject(new Error(`Timeout waiting for ${expectedTypes.join("|")} (${timeoutMs}ms)`)) + }, timeoutMs) + + const entry = { resolve, reject, timer } + // Attach metadata for matching + ;(entry as any)._meta = { types: expectedTypes, filterFn } as PendingMeta + this.pendingResponses.set(key, entry) + }) + } + + // ─── Private: Sending ──────────────────────────────────────── + + private sendFrame(frame: OutgoingFrame): void { + if (!this.ws || !this._isConnected) { + this.messageQueue.push(frame) + return + } + + try { + this.ws.send(JSON.stringify(frame)) + } catch { + this.messageQueue.push(frame) + } + } + + private flushQueue(): void { + while (this.messageQueue.length > 0) { + const frame = this.messageQueue.shift() + if (frame) { + this.sendFrame(frame) + } + } + } + + // ─── Private: Guards ───────────────────────────────────────── + + private ensureRegistered(): void { + if (!this._isRegistered) { + throw new Error("Not registered. Call connect() first.") + } + } + + private notifyConnectionState(state: "connected" | "disconnected" | "reconnecting"): void { + this.connectionStateHandlers.forEach(h => h(state)) + } +} + +/** Internal metadata attached to pending response entries */ +interface PendingMeta { + types: ServerMessageType[] + filterFn?: (frame: IncomingFrame) => boolean +} diff --git a/src/instant_messaging/index.ts b/src/instant_messaging/index.ts index 4bba6f96..0a467288 100644 --- a/src/instant_messaging/index.ts +++ b/src/instant_messaging/index.ts @@ -1,3 +1,16 @@ +// Legacy signaling server client (port 3005) +// For L2PS messaging, use L2PSMessagingPeer instead + +export { L2PSMessagingPeer } from "./L2PSMessagingPeer" +export type { + L2PSMessagingConfig, + L2PSMessageHandler, + L2PSErrorHandler, + L2PSPeerHandler, + L2PSConnectionStateHandler, +} from "./L2PSMessagingPeer" +export * from "./l2ps_types" + // FIXME Integrate with l2ps.ts /** diff --git a/src/instant_messaging/l2ps_types.ts b/src/instant_messaging/l2ps_types.ts new file mode 100644 index 00000000..5fe7f70e --- /dev/null +++ b/src/instant_messaging/l2ps_types.ts @@ -0,0 +1,246 @@ +/** + * L2PS Messaging Protocol Types + * + * WebSocket protocol types for real-time messaging backed by L2PS rollup. + * Messages are delivered instantly via WebSocket and persisted through + * the L2PS batch → proof → L1 pipeline. + * + * These types must stay in sync with the node's L2PS messaging server + * at src/features/l2ps-messaging/types.ts + */ + +// ─── Message Envelope ──────────────────────────────────────────── + +/** The core message envelope that gets encrypted and sent through L2PS */ +export interface MessageEnvelope { + /** Unique message ID (UUID v4) */ + id: string + /** Sender's ed25519 public key (hex) */ + from: string + /** Recipient's ed25519 public key (hex) */ + to: string + /** Message type discriminator */ + type: MessageType + /** Message content (plaintext before E2E encryption) */ + content: string + /** Unix timestamp (ms) when message was created by sender */ + timestamp: number + /** Optional: reply to another message ID */ + replyTo?: string + /** Sender's ed25519 signature of the envelope (hex) */ + signature: string +} + +export type MessageType = + | "text" // Plain text message + | "media" // Media reference (URL/hash) + | "reaction" // Reaction to a message + | "system" // System notification + | "transfer" // Token transfer (future — requires L1 finality) + +// ─── WebSocket Protocol ────────────────────────────────────────── + +/** Client → Server message types */ +export type ClientMessageType = + | "register" + | "send" + | "history" + | "discover" + | "request_public_key" + | "ack" + +/** Server → Client message types */ +export type ServerMessageType = + | "registered" + | "message" + | "message_sent" + | "message_queued" + | "history_response" + | "discover_response" + | "public_key_response" + | "peer_joined" + | "peer_left" + | "error" + +/** Base protocol frame */ +export interface ProtocolFrame { + type: T + payload: Record + timestamp: number +} + +// ─── Client → Server Messages ──────────────────────────────────── + +export interface RegisterMessage extends ProtocolFrame<"register"> { + payload: { + /** Client's ed25519 public key (hex) */ + publicKey: string + /** L2PS network UID to join */ + l2psUid: string + /** Proof: sign("register:{publicKey}:{timestamp}") */ + proof: string + } +} + +export interface SendMessage extends ProtocolFrame<"send"> { + payload: { + /** Recipient's public key (hex) */ + to: string + /** E2E encrypted message envelope (serialized) */ + encrypted: SerializedEncryptedMessage + /** Original message hash for dedup */ + messageHash: string + } +} + +export interface HistoryMessage extends ProtocolFrame<"history"> { + payload: { + /** Peer public key to get conversation with */ + peerKey: string + /** Pagination: messages before this timestamp */ + before?: number + /** Max messages to return */ + limit?: number + /** Proof: sign("history:{peerKey}:{timestamp}") */ + proof: string + } +} + +export interface DiscoverMessage extends ProtocolFrame<"discover"> { + payload: Record +} + +export interface RequestPublicKeyMessage extends ProtocolFrame<"request_public_key"> { + payload: { + /** Target peer's public key or alias */ + targetId: string + } +} + +// ─── Server → Client Messages ──────────────────────────────────── + +export interface RegisteredResponse extends ProtocolFrame<"registered"> { + payload: { + success: boolean + publicKey: string + l2psUid: string + onlinePeers: string[] + } +} + +export interface IncomingMessage extends ProtocolFrame<"message"> { + payload: { + /** Sender's public key */ + from: string + /** E2E encrypted envelope */ + encrypted: SerializedEncryptedMessage + /** Message hash */ + messageHash: string + /** Whether this was delivered from offline storage */ + offline?: boolean + } +} + +export interface MessageSentResponse extends ProtocolFrame<"message_sent"> { + payload: { + messageHash: string + /** L2PS mempool status */ + l2psStatus: "submitted" | "failed" + } +} + +export interface MessageQueuedResponse extends ProtocolFrame<"message_queued"> { + payload: { + messageHash: string + /** Recipient was offline, message queued */ + status: "queued" + } +} + +export interface HistoryResponse extends ProtocolFrame<"history_response"> { + payload: { + messages: StoredMessage[] + hasMore: boolean + } +} + +export interface DiscoverResponse extends ProtocolFrame<"discover_response"> { + payload: { + peers: string[] + } +} + +export interface PublicKeyResponse extends ProtocolFrame<"public_key_response"> { + payload: { + targetId: string + publicKey: string | null + } +} + +export interface PeerJoinedNotification extends ProtocolFrame<"peer_joined"> { + payload: { + publicKey: string + } +} + +export interface PeerLeftNotification extends ProtocolFrame<"peer_left"> { + payload: { + publicKey: string + } +} + +export interface ErrorResponse extends ProtocolFrame<"error"> { + payload: { + code: ErrorCode + message: string + details?: string + } +} + +// ─── Encryption Types ──────────────────────────────────────────── + +/** Serialized E2E encrypted message for wire transport */ +export interface SerializedEncryptedMessage { + /** Encrypted data (base64) */ + ciphertext: string + /** AES-GCM nonce/IV (base64) */ + nonce: string + /** Ephemeral public key for DH (hex) — if using X25519 */ + ephemeralKey?: string +} + +// ─── Storage Types ─────────────────────────────────────────────── + +/** Message as stored in the database / returned by history API */ +export interface StoredMessage { + id: string + from: string + to: string + messageHash: string + encrypted: SerializedEncryptedMessage + l2psUid: string + l2psTxHash: string | null + timestamp: number + status: MessageStatus +} + +export type MessageStatus = + | "delivered" // Sent to recipient via WS + | "queued" // Recipient offline, stored for later delivery + | "sent" // Delivered from offline queue + | "failed" // L2PS submission or persistence failed + | "l2ps_pending" // In L2PS mempool, not yet batched + | "l2ps_batched" // Included in L2PS batch + | "l2ps_confirmed" // Confirmed on L1 + +// ─── Error Codes ───────────────────────────────────────────────── + +export type ErrorCode = + | "INVALID_MESSAGE" + | "REGISTRATION_REQUIRED" + | "INVALID_PROOF" + | "PEER_NOT_FOUND" + | "L2PS_NOT_FOUND" + | "L2PS_SUBMIT_FAILED" + | "RATE_LIMITED" + | "INTERNAL_ERROR" diff --git a/src/types/blockchain/Transaction.ts b/src/types/blockchain/Transaction.ts index b8652b9a..4e9e0172 100644 --- a/src/types/blockchain/Transaction.ts +++ b/src/types/blockchain/Transaction.ts @@ -8,7 +8,7 @@ import { GCREdit } from "./GCREdit" import { INativePayload } from "../native" // import { SubnetPayload } from "../../l2ps" // Obsolete - using new L2PS implementation import { IdentityPayload } from "../abstraction" -import { InstantMessagingPayload } from "../instantMessaging" +import { InstantMessagingPayload, L2PSInstantMessagingPayload } from "../instantMessaging" import { BridgeOperationCompiled, NativeBridgeTxPayload } from "@/bridge/nativeBridgeTypes" import { L2PSEncryptedPayload } from "@/l2ps" import { StoragePayload } from "./TransactionSubtypes/StorageTransaction" @@ -38,6 +38,7 @@ export type TransactionContentData = | ["l2psEncryptedTx", L2PSEncryptedPayload] | ["identity", IdentityPayload] | ["instantMessaging", InstantMessagingPayload] + | ["instantMessaging", L2PSInstantMessagingPayload] | ["nativeBridge", NativeBridgeTxPayload] | ["storage", StoragePayload] | ["storageProgram", StorageProgramPayload] diff --git a/src/types/blockchain/TransactionSubtypes/InstantMessagingTransaction.ts b/src/types/blockchain/TransactionSubtypes/InstantMessagingTransaction.ts index 9488b75c..d52a17ea 100644 --- a/src/types/blockchain/TransactionSubtypes/InstantMessagingTransaction.ts +++ b/src/types/blockchain/TransactionSubtypes/InstantMessagingTransaction.ts @@ -1,5 +1,5 @@ import { Transaction, TransactionContent } from "../Transaction" -import { InstantMessagingPayload } from "@/types/instantMessaging" +import { InstantMessagingPayload, L2PSInstantMessagingPayload } from "@/types/instantMessaging" export type InstantMessagingTransactionContent = Omit & { type: 'instantMessaging' @@ -8,4 +8,14 @@ export type InstantMessagingTransactionContent = Omit { content: InstantMessagingTransactionContent -} \ No newline at end of file +} + +/** L2PS-backed instant messaging transaction */ +export type L2PSInstantMessagingTransactionContent = Omit & { + type: 'instantMessaging' + data: ['instantMessaging', L2PSInstantMessagingPayload] +} + +export interface L2PSInstantMessagingTransaction extends Omit { + content: L2PSInstantMessagingTransactionContent +} \ No newline at end of file diff --git a/src/types/blockchain/TransactionSubtypes/index.ts b/src/types/blockchain/TransactionSubtypes/index.ts index 307e75aa..b800f449 100644 --- a/src/types/blockchain/TransactionSubtypes/index.ts +++ b/src/types/blockchain/TransactionSubtypes/index.ts @@ -25,7 +25,7 @@ import { CrosschainTransaction } from './CrosschainTransaction' import { NativeTransaction } from './NativeTransaction' import { DemosworkTransaction } from './DemosworkTransaction' import { IdentityTransaction } from './IdentityTransaction' -import { InstantMessagingTransaction } from './InstantMessagingTransaction' +import { InstantMessagingTransaction, L2PSInstantMessagingTransaction } from './InstantMessagingTransaction' import { NativeBridgeTransaction } from './NativeBridgeTransaction' import { StorageTransaction } from './StorageTransaction' import { StorageProgramTransaction } from './StorageProgramTransaction' @@ -47,6 +47,7 @@ export type SpecificTransaction = | DemosworkTransaction | IdentityTransaction | InstantMessagingTransaction + | L2PSInstantMessagingTransaction | NativeBridgeTransaction | StorageTransaction | StorageProgramTransaction diff --git a/src/types/instantMessaging/index.ts b/src/types/instantMessaging/index.ts index db3a3888..9bd05c69 100644 --- a/src/types/instantMessaging/index.ts +++ b/src/types/instantMessaging/index.ts @@ -6,3 +6,22 @@ export interface InstantMessagingPayload { messageHash: string } } + +/** L2PS-backed instant messaging transaction payload */ +export interface L2PSInstantMessagingPayload { + type: "instantMessaging" + data: { + /** UUID v4 message identifier */ + messageId: string + /** SHA256 hash for dedup */ + messageHash: string + /** E2E encrypted message (serialized) */ + encrypted: { + ciphertext: string + nonce: string + ephemeralKey?: string + } + /** Unix timestamp (ms) */ + timestamp: number + } +} From a9ac32124145c8ef8dafb66b0997307ff48d7e83 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:40:16 +0000 Subject: [PATCH 2/6] fix: apply CodeRabbit auto-fixes Fixed 4 file(s) based on 4 unresolved review comments. Co-authored-by: CodeRabbit --- src/instant_messaging/L2PSMessagingPeer.ts | 109 +++++++++++++----- src/instant_messaging/l2ps_types.ts | 4 +- src/types/blockchain/Transaction.ts | 5 +- .../InstantMessagingTransaction.ts | 4 +- 4 files changed, 89 insertions(+), 33 deletions(-) diff --git a/src/instant_messaging/L2PSMessagingPeer.ts b/src/instant_messaging/L2PSMessagingPeer.ts index 1c1eb328..2686385b 100644 --- a/src/instant_messaging/L2PSMessagingPeer.ts +++ b/src/instant_messaging/L2PSMessagingPeer.ts @@ -52,12 +52,14 @@ interface OutgoingFrame { type: ClientMessageType payload: Record timestamp: number + requestId?: string } interface IncomingFrame { type: ServerMessageType payload: any timestamp: number + requestId?: string } // ─── Client Class ──────────────────────────────────────────────── @@ -91,6 +93,7 @@ export class L2PSMessagingPeer { private baseReconnectDelay = 1000 private reconnectTimeout: NodeJS.Timeout | null = null private shouldReconnect = true + private isReconnecting = false constructor(config: L2PSMessagingConfig) { this.config = config @@ -118,7 +121,16 @@ export class L2PSMessagingPeer { */ async connect(): Promise { return new Promise((resolve, reject) => { + let checkOpen: NodeJS.Timeout | null = null + const timeout = setTimeout(() => { + if (checkOpen) { + clearInterval(checkOpen) + } + this.shouldReconnect = false + if (this.ws) { + this.ws.close() + } reject(new Error("Connection timeout (10s)")) }, 10000) @@ -126,9 +138,9 @@ export class L2PSMessagingPeer { this.connectWebSocket() // Wait for WS open, then register - const checkOpen = setInterval(() => { + checkOpen = setInterval(() => { if (this._isConnected) { - clearInterval(checkOpen) + clearInterval(checkOpen!) this.register() .then(response => { clearTimeout(timeout) @@ -183,19 +195,19 @@ export class L2PSMessagingPeer { ): Promise { this.ensureRegistered() + const requestId = this.generateRequestId() this.sendFrame({ type: "send", payload: { to, encrypted, messageHash }, timestamp: Date.now(), + requestId, }) // Wait for either message_sent or message_queued return this.waitForResponse( - `send:${messageHash}`, + requestId, ["message_sent", "message_queued"], 15000, - (frame: IncomingFrame) => - frame.payload?.messageHash === messageHash, ) } @@ -215,6 +227,7 @@ export class L2PSMessagingPeer { const proofString = `history:${peerKey}:${timestamp}` const proof = await this.config.signFn(proofString) + const requestId = this.generateRequestId() this.sendFrame({ type: "history", payload: { @@ -224,10 +237,11 @@ export class L2PSMessagingPeer { proof, }, timestamp, + requestId, }) return this.waitForResponse( - `history:${peerKey}`, + requestId, ["history_response"], 10000, ) @@ -240,14 +254,16 @@ export class L2PSMessagingPeer { async discover(): Promise { this.ensureRegistered() + const requestId = this.generateRequestId() this.sendFrame({ type: "discover", payload: {}, timestamp: Date.now(), + requestId, }) const response = await this.waitForResponse( - "discover", + requestId, ["discover_response"], 10000, ) @@ -264,17 +280,18 @@ export class L2PSMessagingPeer { async requestPublicKey(targetId: string): Promise { this.ensureRegistered() + const requestId = this.generateRequestId() this.sendFrame({ type: "request_public_key", payload: { targetId }, timestamp: Date.now(), + requestId, }) const response = await this.waitForResponse( - `pubkey:${targetId}`, + requestId, ["public_key_response"], 10000, - (frame: IncomingFrame) => frame.payload?.targetId === targetId, ) return response.publicKey @@ -332,11 +349,34 @@ export class L2PSMessagingPeer { this.ws = new WebSocket(this.config.serverUrl) this.notifyConnectionState("reconnecting") - this.ws.onopen = () => { + this.ws.onopen = async () => { this._isConnected = true this.reconnectAttempts = 0 - this.notifyConnectionState("connected") - this.flushQueue() + + // Attempt re-registration if this is a reconnection (not initial connection) + if (this.isReconnecting) { + try { + await this.register() + this.notifyConnectionState("connected") + this.flushQueue() + this.isReconnecting = false + } catch (err) { + // Re-registration failed, close and retry + this._isConnected = false + this._isRegistered = false + this.notifyConnectionState("disconnected") + if (this.ws) { + this.ws.close() + } + if (this.shouldReconnect) { + this.attemptReconnect() + } + } + } else { + // Initial connection, registration will be handled by connect() + this.notifyConnectionState("connected") + this.flushQueue() + } } this.ws.onclose = () => { @@ -372,6 +412,7 @@ export class L2PSMessagingPeer { return } + this.isReconnecting = true const delay = Math.min( this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts), 30000, @@ -390,6 +431,7 @@ export class L2PSMessagingPeer { const proofString = `register:${this.config.publicKey}:${timestamp}` const proof = await this.config.signFn(proofString) + const requestId = this.generateRequestId() this.sendFrame({ type: "register", payload: { @@ -398,10 +440,11 @@ export class L2PSMessagingPeer { proof, }, timestamp, + requestId, }) const response = await this.waitForResponse( - "register", + requestId, ["registered"], 10000, ) @@ -414,19 +457,27 @@ export class L2PSMessagingPeer { // ─── Private: Frame Handling ───────────────────────────────── private handleFrame(frame: IncomingFrame): void { - // First, check if any pending response matches - for (const [key, pending] of this.pendingResponses) { + // First, check if any pending response matches by requestId + if (frame.requestId && this.pendingResponses.has(frame.requestId)) { + const pending = this.pendingResponses.get(frame.requestId)! const meta = (pending as any)._meta as PendingMeta | undefined if (meta && meta.types.includes(frame.type)) { - if (!meta.filterFn || meta.filterFn(frame)) { - clearTimeout(pending.timer) - this.pendingResponses.delete(key) - pending.resolve(frame.payload) - return - } + clearTimeout(pending.timer) + this.pendingResponses.delete(frame.requestId) + pending.resolve(frame.payload) + return } } + // Handle error frames with requestId + if (frame.type === "error" && frame.requestId && this.pendingResponses.has(frame.requestId)) { + const pending = this.pendingResponses.get(frame.requestId)! + clearTimeout(pending.timer) + this.pendingResponses.delete(frame.requestId) + pending.reject(new Error(frame.payload?.message || "Server error")) + return + } + // Then dispatch to event handlers switch (frame.type) { case "message": @@ -452,21 +503,20 @@ export class L2PSMessagingPeer { // ─── Private: Request-Response ─────────────────────────────── private waitForResponse( - key: string, + requestId: string, expectedTypes: ServerMessageType[], timeoutMs: number, - filterFn?: (frame: IncomingFrame) => boolean, ): Promise { return new Promise((resolve, reject) => { const timer = setTimeout(() => { - this.pendingResponses.delete(key) + this.pendingResponses.delete(requestId) reject(new Error(`Timeout waiting for ${expectedTypes.join("|")} (${timeoutMs}ms)`)) }, timeoutMs) const entry = { resolve, reject, timer } // Attach metadata for matching - ;(entry as any)._meta = { types: expectedTypes, filterFn } as PendingMeta - this.pendingResponses.set(key, entry) + ;(entry as any)._meta = { types: expectedTypes } as PendingMeta + this.pendingResponses.set(requestId, entry) }) } @@ -505,10 +555,13 @@ export class L2PSMessagingPeer { private notifyConnectionState(state: "connected" | "disconnected" | "reconnecting"): void { this.connectionStateHandlers.forEach(h => h(state)) } + + private generateRequestId(): string { + return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + } } /** Internal metadata attached to pending response entries */ interface PendingMeta { types: ServerMessageType[] - filterFn?: (frame: IncomingFrame) => boolean -} +} \ No newline at end of file diff --git a/src/instant_messaging/l2ps_types.ts b/src/instant_messaging/l2ps_types.ts index 5fe7f70e..67c1959c 100644 --- a/src/instant_messaging/l2ps_types.ts +++ b/src/instant_messaging/l2ps_types.ts @@ -67,6 +67,8 @@ export interface ProtocolFrame { type: T payload: Record timestamp: number + /** Request correlation ID for request/response flows */ + requestId?: string } // ─── Client → Server Messages ──────────────────────────────────── @@ -243,4 +245,4 @@ export type ErrorCode = | "L2PS_NOT_FOUND" | "L2PS_SUBMIT_FAILED" | "RATE_LIMITED" - | "INTERNAL_ERROR" + | "INTERNAL_ERROR" \ No newline at end of file diff --git a/src/types/blockchain/Transaction.ts b/src/types/blockchain/Transaction.ts index 4e9e0172..586831e5 100644 --- a/src/types/blockchain/Transaction.ts +++ b/src/types/blockchain/Transaction.ts @@ -38,7 +38,7 @@ export type TransactionContentData = | ["l2psEncryptedTx", L2PSEncryptedPayload] | ["identity", IdentityPayload] | ["instantMessaging", InstantMessagingPayload] - | ["instantMessaging", L2PSInstantMessagingPayload] + | ["l2psInstantMessaging", L2PSInstantMessagingPayload] | ["nativeBridge", NativeBridgeTxPayload] | ["storage", StoragePayload] | ["storageProgram", StorageProgramPayload] @@ -64,6 +64,7 @@ export interface TransactionContent { | "NODE_ONLINE" | "identity" | "instantMessaging" + | "l2psInstantMessaging" | "nativeBridge" | "l2psEncryptedTx" | "storage" @@ -106,4 +107,4 @@ export interface Transaction { } // Re-export specific transaction types -export * from './TransactionSubtypes' +export * from './TransactionSubtypes' \ No newline at end of file diff --git a/src/types/blockchain/TransactionSubtypes/InstantMessagingTransaction.ts b/src/types/blockchain/TransactionSubtypes/InstantMessagingTransaction.ts index d52a17ea..2bd3e0ad 100644 --- a/src/types/blockchain/TransactionSubtypes/InstantMessagingTransaction.ts +++ b/src/types/blockchain/TransactionSubtypes/InstantMessagingTransaction.ts @@ -12,8 +12,8 @@ export interface InstantMessagingTransaction extends Omit & { - type: 'instantMessaging' - data: ['instantMessaging', L2PSInstantMessagingPayload] + type: 'l2psInstantMessaging' + data: ['l2psInstantMessaging', L2PSInstantMessagingPayload] } export interface L2PSInstantMessagingTransaction extends Omit { From e389263ccfc4c1e6420c6d210558eea1554ea406 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 26 Mar 2026 16:50:56 +0100 Subject: [PATCH 3/6] added myc and serena --- .mycelium/.gitignore | 1 + .mycelium/mycelium.db | Bin 69632 -> 102400 bytes .serena/project.yml | 14 ++++++++++++++ 3 files changed, 15 insertions(+) diff --git a/.mycelium/.gitignore b/.mycelium/.gitignore index f7d55a5c..c1baa23f 100644 --- a/.mycelium/.gitignore +++ b/.mycelium/.gitignore @@ -4,3 +4,4 @@ *.db-shm # Temporary files *.tmp +.linear/ diff --git a/.mycelium/mycelium.db b/.mycelium/mycelium.db index 30586269fe9cc369ae01888505b673b028383b4f..ca684488664ce245f9a3c6d2c4199aae1a71fd4e 100644 GIT binary patch literal 102400 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 diff --git a/.serena/project.yml b/.serena/project.yml index 0eb31e34..968c5af4 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -132,3 +132,17 @@ line_ending: # list of regex patterns which, when matched, mark a memory entry as read‑only. # Extends the list from the global configuration, merging the two lists. read_only_memory_patterns: [] + +# advanced configuration option allowing to configure language server-specific options. +# Maps the language key to the options. +# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. +# No documentation on options means no options are available. +ls_specific_settings: {} + +# list of regex patterns for memories to completely ignore. +# Matching memories will not appear in list_memories or activate_project output +# and cannot be accessed via read_memory or write_memory. +# To access ignored memory files, use the read_file tool on the raw file path. +# Extends the list from the global configuration, merging the two lists. +# Example: ["_archive/.*", "_episodes/.*"] +ignored_memory_patterns: [] From c5374bfe614adb4c5e77e0200d28e463ae640dee Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 26 Mar 2026 16:51:18 +0100 Subject: [PATCH 4/6] fixed agents --- AGENTS.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index ea431746..ed270ff0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,7 +135,6 @@ history/ For more details, see README.md and QUICKSTART.md. - ## Project Management with Mycelium This project uses [Mycelium](https://github.com/tcsenpai/mycelium) (`myc`) for task and epic management. @@ -211,3 +210,55 @@ When working on this project: 3. Create tasks for new work: `myc task create --title "..." --description "..." --epic N` 4. Mark tasks complete when done: `myc task close N` 5. Use `--format json` for machine-readable output: `myc task list --format json` + +## Mental Frameworks for Mycelium Usage + +### 1. INVEST — Task Quality Gate + +Before creating or updating any task, validate it against these criteria. +A task that fails more than one is not ready to be written. + +| Criterion | Rule | +|---|---| +| **Independent** | Can be completed without unblocking other tasks first | +| **Negotiable** | The *what* is fixed; the *how* remains open | +| **Valuable** | Produces a verifiable, concrete outcome | +| **Estimable** | If you cannot size it, it is too vague or too large | +| **Small** | If it spans more than one work cycle, split it | +| **Testable** | Has an explicit, binary done condition | + +> If a task fails **Estimable** or **Testable**, convert it to an Epic and decompose. + +--- + +### 2. DAG — Dependency Graph Thinking + +Before scheduling or prioritizing, model the implicit dependency graph. + +**Rules:** +- No task moves to `in_progress` if it has an unresolved upstream blocker +- Priority is a function of both urgency **and fan-out** (how many tasks does completing this one unlock?) +- Always work the **critical path** first — not the task that feels most urgent + +**Prioritization heuristic:** +``` +score = urgency + (blocked_tasks_count × 1.5) +``` + +When creating a task, explicitly ask: *"What does this block, and what blocks this?"* +Set dependency links in Mycelium before touching status. + +--- + +### 3. Principle of Minimal Surprise (PMS) + +Mycelium's state must remain predictable and auditable at all times. + +**Rules:** +- **Prefer idempotent operations** — update before you create; never duplicate +- **Check before write** — search for an equivalent item before creating a new one +- **Always annotate mutations** — every status change, priority shift, or reassignment must carry an explicit `reason` field +- **No orphan tasks** — every task must be linked to an Epic; every Epic to a strategic goal +- Deletions are a last resort; prefer `cancelled` status with a reason + +> The state of Mycelium after any operation must be explainable to another agent with zero context. From ec99a6650c543b1f004084d2464d8c846794c5a3 Mon Sep 17 00:00:00 2001 From: tcsenpai Date: Thu, 26 Mar 2026 16:51:42 +0100 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20address=20PR=20#82=20review=20commen?= =?UTF-8?q?ts=20=E2=80=94=20type=20safety=20and=20minor=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace IncomingFrame.payload: any with ServerPayload discriminated union - Eliminate (entry as any)._meta casts by typing pendingResponses map properly - Add type-narrowing casts in handleFrame switch branches - Notify error handlers when re-registration fails after reconnect - Fix WebSocket error stringification (String(event) → proper extraction) - Replace deprecated substr() with substring() --- .mycelium/mycelium.db | Bin 102400 -> 102400 bytes src/instant_messaging/L2PSMessagingPeer.ts | 69 ++++++++++++++------- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/.mycelium/mycelium.db b/.mycelium/mycelium.db index ca684488664ce245f9a3c6d2c4199aae1a71fd4e..0d61d4676c1821ff5a8caea04f3272b6a383bec5 100644 GIT binary patch delta 163 zcmZozz}B#UZ3AlnE2A<4qw;3107*v978^!hhQhW?$<1eCpEFKYjFg)EE?#Kz-FQAx zQ$s65b3F@FLjxld15*oa14AnVgUw07m5fZhhd191dB(xQz`$g?`FN~-kia5l{zd%T z`QP$C;=jVbhX3SdL4zKCB~E5Z&g7i@;?xu_W=Tc}iw(hK;{P%E$bJ)`F$^4=|LkYE F004j{G86y+ delta 109 zcmZozz}B#UZ3AlnD{~S9@8Qi{0g{ZH6$I>=HYWvFGBPnRY`z=vjDw}cijjBo@mTvH zfi=wh+xg$}KjOc_zlQ%L|Buar2GjT@xR@mwlXLQmQ&ZRwOrY+`NA{brurrHtZvL~M G = new Set() // Pending request-response waiters + // REVIEW: resolve accepts ServerPayload (widened from generic T) because the map is homogeneous private pendingResponses: Map< string, - { resolve: (value: any) => void; reject: (error: Error) => void; timer: NodeJS.Timeout } + { resolve: (value: unknown) => void; reject: (error: Error) => void; timer: NodeJS.Timeout; _meta?: PendingMeta } > = new Map() // State @@ -361,7 +375,10 @@ export class L2PSMessagingPeer { this.flushQueue() this.isReconnecting = false } catch (err) { - // Re-registration failed, close and retry + // Re-registration failed — notify error handlers and retry + this.errorHandlers.forEach(h => + h({ code: "INTERNAL_ERROR" as ErrorCode, message: "Re-registration failed after reconnect", details: String(err) }), + ) this._isConnected = false this._isRegistered = false this.notifyConnectionState("disconnected") @@ -391,7 +408,7 @@ export class L2PSMessagingPeer { this.ws.onerror = (event) => { this.errorHandlers.forEach(h => - h({ code: "INTERNAL_ERROR" as ErrorCode, message: "WebSocket error", details: String(event) }), + h({ code: "INTERNAL_ERROR" as ErrorCode, message: "WebSocket error", details: event instanceof Error ? event.message : "unknown" }), ) } @@ -460,7 +477,7 @@ export class L2PSMessagingPeer { // First, check if any pending response matches by requestId if (frame.requestId && this.pendingResponses.has(frame.requestId)) { const pending = this.pendingResponses.get(frame.requestId)! - const meta = (pending as any)._meta as PendingMeta | undefined + const meta = pending._meta if (meta && meta.types.includes(frame.type)) { clearTimeout(pending.timer) this.pendingResponses.delete(frame.requestId) @@ -474,26 +491,34 @@ export class L2PSMessagingPeer { const pending = this.pendingResponses.get(frame.requestId)! clearTimeout(pending.timer) this.pendingResponses.delete(frame.requestId) - pending.reject(new Error(frame.payload?.message || "Server error")) + pending.reject(new Error((frame.payload as ErrorResponse["payload"])?.message || "Server error")) return } // Then dispatch to event handlers switch (frame.type) { - case "message": - this.messageHandlers.forEach(h => h(frame.payload)) + case "message": { + const p = frame.payload as IncomingMessage["payload"] + this.messageHandlers.forEach(h => h(p)) break - case "peer_joined": - this.onlinePeers.add(frame.payload.publicKey) - this.peerJoinedHandlers.forEach(h => h(frame.payload.publicKey)) + } + case "peer_joined": { + const p = frame.payload as PeerJoinedNotification["payload"] + this.onlinePeers.add(p.publicKey) + this.peerJoinedHandlers.forEach(h => h(p.publicKey)) break - case "peer_left": - this.onlinePeers.delete(frame.payload.publicKey) - this.peerLeftHandlers.forEach(h => h(frame.payload.publicKey)) + } + case "peer_left": { + const p = frame.payload as PeerLeftNotification["payload"] + this.onlinePeers.delete(p.publicKey) + this.peerLeftHandlers.forEach(h => h(p.publicKey)) break - case "error": - this.errorHandlers.forEach(h => h(frame.payload)) + } + case "error": { + const p = frame.payload as ErrorResponse["payload"] + this.errorHandlers.forEach(h => h(p)) break + } default: // message_sent, message_queued etc. without a waiter — ignore break @@ -513,10 +538,12 @@ export class L2PSMessagingPeer { reject(new Error(`Timeout waiting for ${expectedTypes.join("|")} (${timeoutMs}ms)`)) }, timeoutMs) - const entry = { resolve, reject, timer } - // Attach metadata for matching - ;(entry as any)._meta = { types: expectedTypes } as PendingMeta - this.pendingResponses.set(requestId, entry) + this.pendingResponses.set(requestId, { + resolve: resolve as (value: unknown) => void, + reject, + timer, + _meta: { types: expectedTypes }, + }) }) } @@ -557,7 +584,7 @@ export class L2PSMessagingPeer { } private generateRequestId(): string { - return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + return `req_${Date.now()}_${Math.random().toString(36).substring(2, 11)}` } } From 8cee729c6ea8bf930d26a2e5581970aadf154f46 Mon Sep 17 00:00:00 2001 From: TheCookingSenpai <153772003+tcsenpai@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:57:07 +0100 Subject: [PATCH 6/6] Update src/instant_messaging/L2PSMessagingPeer.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/instant_messaging/L2PSMessagingPeer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instant_messaging/L2PSMessagingPeer.ts b/src/instant_messaging/L2PSMessagingPeer.ts index ce11654a..093df573 100644 --- a/src/instant_messaging/L2PSMessagingPeer.ts +++ b/src/instant_messaging/L2PSMessagingPeer.ts @@ -377,7 +377,7 @@ export class L2PSMessagingPeer { } catch (err) { // Re-registration failed — notify error handlers and retry this.errorHandlers.forEach(h => - h({ code: "INTERNAL_ERROR" as ErrorCode, message: "Re-registration failed after reconnect", details: String(err) }), + h({ code: "INTERNAL_ERROR" as ErrorCode, message: "Re-registration failed after reconnect", details: err instanceof Error ? err.message : "unknown" }), ) this._isConnected = false this._isRegistered = false