import { DAppProvider } from "wallet-comms-sdk"; import type { SessionInfo, AccountDTO, KeyPath, AppNotification, DappRpcRequest } from "../types"; import type { WalletDB } from "../persistence"; import { AccountType, KeySpace, MAIN_WALLET_ID, SessionRequestType } from "../utils"; import { Networks } from "libnexa-ts"; import type { KeyManager } from "./key"; type MethodHandler = { resolve: (value: any) => void; reject: (value: any) => void; } export type SessionEvent = | { type: 'session_added'; accountId: number; sessionInfo: SessionInfo } | { type: 'session_removed'; accountId: number; sessionId: string } | { type: 'sessions_cleared', accountId: number } | { type: 'new_notification'; notification: AppNotification } | { type: 'new_request'; request: DappRpcRequest }; export type SessionUpdateCallback = (event: SessionEvent) => void; export class SessionManager { private readonly walletDb: WalletDB; private readonly keyManager: KeyManager; private readonly providers: Map>; private readonly handlers: Map; private removeOnClose: boolean; private updateCallback?: SessionUpdateCallback; public constructor(walletDb: WalletDB, keyManager: KeyManager) { this.walletDb = walletDb; this.keyManager = keyManager; this.providers = new Map(); this.handlers = new Map(); this.removeOnClose = true; } public onUpdate(callback: SessionUpdateCallback): void { this.updateCallback = callback; } private notify(event: SessionEvent): void { this.updateCallback?.(event); } public getHandler(request: string): MethodHandler | undefined { return this.handlers.get(request); } public add(account: AccountDTO, provider: DAppProvider, sessionInfo: SessionInfo): void { if (!this.providers.has(account.id)) { this.providers.set(account.id, new Map()); } const sessionId = sessionInfo.details.sessionId; const accountSessions = this.providers.get(account.id)!; this.registerHandlers(account, provider, sessionId); accountSessions.set(sessionId, provider); this.notify({ type: 'session_added', accountId: account.id, sessionInfo }); } public remove(accountId: number, sessionId: string): void { const sessionMap = this.providers.get(accountId); if (sessionMap) { const provider = sessionMap.get(sessionId); if (provider) { provider.disconnect(); sessionMap.delete(sessionId); } if (sessionMap.size === 0) { this.providers.delete(accountId); } } this.notify({ type: 'session_removed', accountId, sessionId }); } public async reload(accounts: Map): Promise { for (const account of accounts.values()) { if (account.id == MAIN_WALLET_ID) { continue; } const storedSessions = await this.walletDb.getAccountSessions(account.id); const currentSessions = this.providers.get(account.id); const loadedSessions = storedSessions.map(async (session) => { if (currentSessions?.has(session.sessionId)) { return; } let provider; try { provider = new DAppProvider(session.uri); const details = provider.getSessionInfo(); await provider.connect(3000); const appInfo = await provider.getAppInfo(2000); await provider.joinSession(account.address, 2000); const messages = await provider.fetchPendingMessages(); for (const msg of messages) { this.notify({ type: 'new_notification', notification: { id: crypto.randomUUID(), createdAt: msg.createdAt, type: 'web3', title: 'Request pending approval', message: `A connected dApp (${appInfo.name}) has requested an action from your Account: ${account.name}. Review the request details before approving or rejecting.`, action: { type: 'DAPP_REQUEST', account: account.id, sessionId: details.sessionId, payload: msg.message } } }); } return { account: account, provider, metadata: { details, appInfo } }; } catch (error) { console.error(`Failed to reload session ${session.sessionId}`, error); provider?.disconnect(); await this.walletDb.removeSession(session.sessionId); } }); const results= await Promise.allSettled(loadedSessions); for (const result of results) { if (result.status === 'fulfilled' && result.value) { this.add(result.value.account, result.value.provider, result.value.metadata); } } } } public clear(): void { try { this.removeOnClose = false; for (const [accountId, sessions] of this.providers) { for (const [,provider] of sessions) { provider.disconnect(); } sessions.clear(); this.notify({ type: 'sessions_cleared', accountId }); } this.providers.clear(); } finally { this.removeOnClose = true; } } public async revoke(accountId: number, sessionId: string): Promise { const sessionMap = this.providers.get(accountId); if (sessionMap) { const provider = sessionMap.get(sessionId); if (provider) { await provider.revokeSession(); } } } public async replayMessage(accountId: number, sessionId: string, payload: string): Promise { const sessionMap = this.providers.get(accountId); if (sessionMap) { const provider = sessionMap.get(sessionId); if (provider) { await provider.replayMessage(payload); } } } private registerHandlers(account: AccountDTO, provider: DAppProvider, sessionId: string): void { provider.onSessionDelete((): Promise => { return this.walletDb.removeSession(sessionId); }); provider.onClose((): void => { if (this.removeOnClose) { this.remove(account.id, sessionId); } }); const handleRequest = (type: SessionRequestType, request: unknown): Promise => { return new Promise((resolve, reject) => { this.handlers.set(type, { resolve, reject }); this.notify({ type: 'new_request', request: { type, accountId: account.id, sessionId, request } }); }); } provider.onSignMessage(signMsgReq => { return handleRequest(SessionRequestType.SignMessage, signMsgReq); }); provider.onAddToken(addTokenReq => { return handleRequest(SessionRequestType.AddToken, addTokenReq); }); provider.onSendTransaction(sendTransactionReq => { return handleRequest(SessionRequestType.SendTransaction, sendTransactionReq); }); provider.onSignTransaction(signTransactionReq => { return handleRequest(SessionRequestType.SignTransaction, signTransactionReq); }); provider.onGetAccount(() => { const path: KeyPath = { account: AccountType.DAPP, type: KeySpace.RECEIVE, index: account.id }; return { name: account.name, address: account.address, pubkey: this.keyManager.getKey(path).publicKey.toString(), blockchain: "nexa", network: Networks.defaultNetwork.name, capabilities: { addToken: true, sendTransaction: true, signMessage: true, signTransaction: true } } }); } }