diff --git a/packages/walletkit-ios-bridge/src/SwiftTONConnectSessionsManager.ts b/packages/walletkit-ios-bridge/src/SwiftTONConnectSessionsManager.ts new file mode 100644 index 00000000..ff2fc3fd --- /dev/null +++ b/packages/walletkit-ios-bridge/src/SwiftTONConnectSessionsManager.ts @@ -0,0 +1,66 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { TONConnectSessionManager, TONConnectSession, DAppInfo, Wallet, WalletId } from '@ton/walletkit'; + +/** + * Swift adapter for TONConnect session management. + * Delegates all session operations to the Swift implementation. + */ +export class SwiftTONConnectSessionsManager implements TONConnectSessionManager { + private swiftSessionsManager: TONConnectSessionManager; + + constructor(swiftSessionsManager: TONConnectSessionManager) { + this.swiftSessionsManager = swiftSessionsManager; + } + + async initialize(): Promise { + /* + No initialization needed for Swift implementation. + This method needed to comply with the TONConnectSessionManager interface. + Such compliance is needed for backward compability with existing codebase. + */ + } + + async createSession( + sessionId: string, + dAppInfo: DAppInfo, + wallet: Wallet, + isJsBridge: boolean, + ): Promise { + return await this.swiftSessionsManager.createSession(sessionId, dAppInfo, wallet, isJsBridge); + } + + async getSession(sessionId: string): Promise { + return await this.swiftSessionsManager.getSession(sessionId); + } + + async getSessionByDomain(domain: string): Promise { + return await this.swiftSessionsManager.getSessionByDomain(domain); + } + + async getSessions(): Promise { + return await this.swiftSessionsManager.getSessions(); + } + + async getSessionsForWallet(walletId: WalletId): Promise { + return await this.swiftSessionsManager.getSessionsForWallet(walletId); + } + + async removeSession(sessionId: string): Promise { + await this.swiftSessionsManager.removeSession(sessionId); + } + + async removeSessionsForWallet(walletId: WalletId): Promise { + await this.swiftSessionsManager.removeSessionsForWallet(walletId); + } + + async clearSessions(): Promise { + await this.swiftSessionsManager.clearSessions(); + } +} diff --git a/packages/walletkit-ios-bridge/src/main.ts b/packages/walletkit-ios-bridge/src/main.ts index 1be7fa48..6d412f2c 100644 --- a/packages/walletkit-ios-bridge/src/main.ts +++ b/packages/walletkit-ios-bridge/src/main.ts @@ -16,15 +16,22 @@ import type { WalletAdapter } from '@ton/walletkit'; import { SwiftStorageAdapter } from './SwiftStorageAdapter'; import { SwiftWalletAdapter } from './SwiftWalletAdapter'; import { SwiftAPIClientAdapter } from './SwiftAPIClientAdapter'; +import { SwiftTONConnectSessionsManager } from './SwiftTONConnectSessionsManager'; declare global { interface Window { walletKit?: any; - initWalletKit: (configuration, storage, bridgeTransport: (response) => void, apiClients) => Promise; + initWalletKit: ( + configuration, + storage, + bridgeTransport: (response) => void, + sessionManager, + apiClients, + ) => Promise; } } -window.initWalletKit = async (configuration, storage, bridgeTransport, apiClients) => { +window.initWalletKit = async (configuration, storage, bridgeTransport, sessionManager, apiClients) => { console.log('🚀 WalletKit iOS Bridge starting...'); console.log('Creating WalletKit instance with configuration', configuration); @@ -59,6 +66,7 @@ window.initWalletKit = async (configuration, storage, bridgeTransport, apiClient networks, walletManifest: configuration.walletManifest, deviceInfo: configuration.deviceInfo, + sessionManager: sessionManager ? new SwiftTONConnectSessionsManager(sessionManager) : undefined, bridge: configuration.bridge, eventProcessor: configuration.eventsConfiguration, storage: storage ? new SwiftStorageAdapter(storage) : new MemoryStorageAdapter({}), diff --git a/packages/walletkit/src/api/interfaces/TONConnectSessionManager.ts b/packages/walletkit/src/api/interfaces/TONConnectSessionManager.ts new file mode 100644 index 00000000..54baa16f --- /dev/null +++ b/packages/walletkit/src/api/interfaces/TONConnectSessionManager.ts @@ -0,0 +1,80 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +// TONConnect Session Manager abstraction for Dependency Inversion + +import type { DAppInfo, TONConnectSession } from '../models'; +import type { WalletId } from '../../utils/walletId'; +import type { Wallet } from '.'; + +/** + * Abstraction for session management in TONConnect protocol. + * Provides interface for session CRUD operations and lifecycle management. + */ +export interface TONConnectSessionManager { + /** + * Initialize the session manager + * Needed for backward compatibility with existing codebase + * Used to ensure that sessions are reloaded fro mstorage to local cache in browser extension + * */ + initialize(): Promise; + + /** + * Create a new session + * @param sessionId - Unique session identifier + * @param dAppInfo - Information about the dApp (name, url, iconUrl, description) + * @param wallet - The wallet to associate with this session + * @param options - Additional options for session creation + */ + createSession( + sessionId: string, + dAppInfo: DAppInfo, + wallet: Wallet, + isJsBridge: boolean, + ): Promise; + + /** + * Get session by ID + * @param sessionId - The session ID to retrieve + */ + getSession(sessionId: string): Promise; + + /** + * Get session by domain + * @param domain - The domain to search for + */ + getSessionByDomain(domain: string): Promise; + + /** + * Get all sessions as array + */ + getSessions(): Promise; + + /** + * Get sessions for specific wallet by wallet ID + * @param walletId - The wallet ID to filter by + */ + getSessionsForWallet(walletId: WalletId): Promise; + + /** + * Remove session by ID + * @param sessionId - The session ID to remove + */ + removeSession(sessionId: string): Promise; + + /** + * Remove all sessions for a wallet by wallet ID + * @param walletId - Wallet ID string + */ + removeSessionsForWallet(walletId: WalletId): Promise; + + /** + * Clear all sessions + */ + clearSessions(): Promise; +} diff --git a/packages/walletkit/src/api/interfaces/index.ts b/packages/walletkit/src/api/interfaces/index.ts index 240e61dd..9194d913 100644 --- a/packages/walletkit/src/api/interfaces/index.ts +++ b/packages/walletkit/src/api/interfaces/index.ts @@ -9,3 +9,4 @@ export type { Wallet, WalletTonInterface, WalletNftInterface, WalletJettonInterface } from './Wallet'; export type { WalletAdapter } from './WalletAdapter'; export type { WalletSigner, ISigner } from './WalletSigner'; +export type { TONConnectSessionManager } from './TONConnectSessionManager'; diff --git a/packages/walletkit/src/api/models/bridge/TONConnectSession.ts b/packages/walletkit/src/api/models/bridge/TONConnectSession.ts new file mode 100644 index 00000000..5935c698 --- /dev/null +++ b/packages/walletkit/src/api/models/bridge/TONConnectSession.ts @@ -0,0 +1,48 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { UserFriendlyAddress, WalletId } from '../../..'; + +export interface TONConnectSession { + sessionId: string; + + walletId: WalletId; + walletAddress: UserFriendlyAddress; + createdAt: string; // date + lastActivityAt: string; // date + privateKey: string; + publicKey: string; + domain: string; + + /** + * Display name of the dApp + */ + dAppName?: string; + + /** + * Brief description of the dApp's purpose + */ + dAppDescription?: string; + + /** + * Main website URL of the dApp + * @format url + */ + dAppUrl?: string; + + /** + * Icon/logo URL of the dApp + * @format url + */ + dAppIconUrl?: string; + + // Bridge type indicator (needed to determine how to send disconnect events) + isJsBridge?: boolean; // true if session was created via JS Bridge, false/undefined for HTTP Bridge + + schemaVersion: number; +} diff --git a/packages/walletkit/src/api/models/index.ts b/packages/walletkit/src/api/models/index.ts index 600525ee..623ddb23 100644 --- a/packages/walletkit/src/api/models/index.ts +++ b/packages/walletkit/src/api/models/index.ts @@ -38,6 +38,7 @@ export type { SignDataRequestEvent, SignDataRequestEventPreview, SignDataPreview export type { TransactionApprovalResponse } from './bridge/TransactionApprovalResponse'; export type { TransactionRequestEvent, TransactionRequestEventPreview } from './bridge/TransactionRequestEvent'; export type { RequestErrorEvent } from './bridge/RequestErrorEvent'; +export type { TONConnectSession } from './bridge/TONConnectSession'; // Jetton models export type { Jetton } from './jettons/Jetton'; diff --git a/packages/walletkit/src/core/BridgeManager.ts b/packages/walletkit/src/core/BridgeManager.ts index b0b0afff..7b75e68c 100644 --- a/packages/walletkit/src/core/BridgeManager.ts +++ b/packages/walletkit/src/core/BridgeManager.ts @@ -12,12 +12,12 @@ import { SessionCrypto } from '@tonconnect/protocol'; import type { ClientConnection, WalletConsumer } from '@tonconnect/bridge-sdk'; import { BridgeProvider } from '@tonconnect/bridge-sdk'; -import type { BridgeConfig, RawBridgeEvent, SessionData } from '../types/internal'; +import type { BridgeConfig, RawBridgeEvent } from '../types/internal'; import type { Storage } from '../storage'; import type { EventStore } from '../types/durableEvents'; import type { EventEmitter } from './EventEmitter'; import { globalLogger } from './Logger'; -import type { SessionManager } from './SessionManager'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; import type { EventRouter } from './EventRouter'; import type { BridgeEventMessageInfo, @@ -30,14 +30,14 @@ import { WalletKitError, ERROR_CODES } from '../errors'; import type { Analytics, AnalyticsManager } from '../analytics'; import type { TonWalletKitOptions } from '../types/config'; import { TONCONNECT_BRIDGE_RESPONSE } from '../bridge/JSBridgeInjector'; -import type { BridgeEvent } from '../api/models'; +import type { BridgeEvent, TONConnectSession } from '../api/models'; const log = globalLogger.createChild('BridgeManager'); export class BridgeManager { private config: BridgeConfig; private bridgeProvider?: BridgeProvider; - private sessionManager: SessionManager; + private sessionManager: TONConnectSessionManager; private storage: Storage; private isConnected = false; private reconnectAttempts = 0; @@ -62,7 +62,7 @@ export class BridgeManager { constructor( walletManifest: WalletInfo | undefined, config: BridgeConfig | undefined, - sessionManager: SessionManager, + sessionManager: TONConnectSessionManager, storage: Storage, eventStore: EventStore, eventRouter: EventRouter, @@ -188,7 +188,7 @@ export class BridgeManager { event: BridgeEvent, // eslint-disable-next-line @typescript-eslint/no-explicit-any response: any, - _session?: SessionData, + providedSessionCrypto?: SessionCrypto, ): Promise { if (event.isLocal) { return; @@ -220,19 +220,25 @@ export class BridgeManager { ); } - const session = _session ?? (await this.sessionManager.getSession(sessionId)); - if (!session) { - throw new WalletKitError(ERROR_CODES.SESSION_NOT_FOUND, `Session not found for response`, undefined, { - sessionId, - eventId: event.id, - }); + let sessionCrypto = providedSessionCrypto; + + if (!sessionCrypto) { + const session = await this.sessionManager.getSession(sessionId); + + if (session) { + sessionCrypto = new SessionCrypto({ + publicKey: session.publicKey, + secretKey: session.privateKey, + }); + } else { + throw new WalletKitError(ERROR_CODES.SESSION_NOT_FOUND, `Session not found for response`, undefined, { + sessionId, + eventId: event.id, + }); + } } try { - const sessionCrypto = new SessionCrypto({ - publicKey: session.publicKey, - secretKey: session.privateKey, - }); await this.bridgeProvider.send(response, sessionCrypto, sessionId, { traceId: event?.traceId, }); @@ -261,7 +267,7 @@ export class BridgeManager { response: any, options?: { traceId?: string; - session?: SessionData; + session?: TONConnectSession; }, ): Promise { const source = this.config.jsBridgeKey + '-tonconnect'; @@ -346,7 +352,7 @@ export class BridgeManager { // } private async getClients(): Promise { - return this.sessionManager.getSessions().map((session) => ({ + return (await this.sessionManager.getSessions()).map((session) => ({ session: new SessionCrypto({ publicKey: session.publicKey, secretKey: session.privateKey.length > 64 ? session.privateKey.slice(0, 64) : session.privateKey, @@ -606,7 +612,7 @@ export class BridgeManager { rawEvent.dAppInfo = { name: session.dAppName, description: session.dAppDescription, - url: session.dAppIconUrl, + url: session.dAppUrl, iconUrl: session.dAppIconUrl, }; } @@ -627,7 +633,7 @@ export class BridgeManager { rawEvent.dAppInfo = { name: session.dAppName, description: session.dAppDescription, - url: session.dAppIconUrl, + url: session.dAppUrl, iconUrl: session.dAppIconUrl, }; diff --git a/packages/walletkit/src/core/EventProcessor.spec.ts b/packages/walletkit/src/core/EventProcessor.spec.ts index ec4a6016..56ee6572 100644 --- a/packages/walletkit/src/core/EventProcessor.spec.ts +++ b/packages/walletkit/src/core/EventProcessor.spec.ts @@ -12,7 +12,7 @@ import { StorageEventProcessor } from './EventProcessor'; import { StorageEventStore } from './EventStore'; import type { DurableEventsConfig } from '../types/durableEvents'; import type { WalletManager } from './WalletManager'; -import type { SessionManager } from './SessionManager'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; import type { EventRouter } from './EventRouter'; import type { EventEmitter } from './EventEmitter'; import type { RawBridgeEvent } from '../types/internal'; @@ -25,7 +25,7 @@ describe('EventProcessor with Real EventStore', () => { let eventStore: StorageEventStore; let storage: Storage; let walletManager: WalletManager; - let sessionManager: SessionManager; + let sessionManager: TONConnectSessionManager; let eventRouter: EventRouter; let eventEmitter: EventEmitter; let config: DurableEventsConfig; @@ -39,16 +39,24 @@ describe('EventProcessor with Real EventStore', () => { // Mock SessionManager sessionManager = { - getSessionsForAPI: vi.fn().mockReturnValue([ + getSessions: vi.fn().mockReturnValue([ { sessionId: 'session-1', walletId: createWalletId(Network.mainnet(), 'wallet-1'), - dAppName: 'Test DApp', - dAppUrl: 'https://test.com', - dAppIconUrl: 'https://test.com/icon.png', + walletAddress: 'wallet-1', + createdAt: new Date().toISOString(), + lastActivityAt: new Date().toISOString(), + privateKey: 'test-private-key', + publicKey: 'test-public-key', + dAppInfo: { + name: 'Test DApp', + url: 'https://test.com', + iconUrl: 'https://test.com/icon.png', + description: 'Test DApp Description', + }, }, ]), - } as unknown as SessionManager; + } as unknown as TONConnectSessionManager; // Mock EventRouter eventRouter = { @@ -436,20 +444,36 @@ describe('EventProcessor with Real EventStore', () => { describe('Multi-Wallet Processing', () => { it('should process events from multiple wallets in chronological order', async () => { // Set up two wallets - vi.mocked(sessionManager.getSessionsForAPI).mockReturnValue([ + vi.mocked(sessionManager.getSessions).mockReturnValue([ { sessionId: 'session-1', walletId: createWalletId(Network.mainnet(), 'wallet-1'), - dAppName: 'Test DApp 1', - dAppUrl: 'https://test1.com', - dAppIconUrl: 'https://test1.com/icon.png', + walletAddress: 'wallet-1', + createdAt: new Date().toISOString(), + lastActivityAt: new Date().toISOString(), + privateKey: 'test-private-key-1', + publicKey: 'test-public-key-1', + dAppInfo: { + name: 'Test DApp 1', + url: 'https://test1.com', + iconUrl: 'https://test1.com/icon.png', + description: 'Test DApp 1 Description', + }, }, { sessionId: 'session-2', walletId: createWalletId(Network.mainnet(), 'wallet-2'), - dAppName: 'Test DApp 2', - dAppUrl: 'https://test2.com', - dAppIconUrl: 'https://test2.com/icon.png', + walletAddress: 'wallet-2', + createdAt: new Date().toISOString(), + lastActivityAt: new Date().toISOString(), + privateKey: 'test-private-key-2', + publicKey: 'test-public-key-2', + dAppInfo: { + name: 'Test DApp 2', + url: 'https://test2.com', + iconUrl: 'https://test2.com/icon.png', + description: 'Test DApp 2 Description', + }, }, ]); diff --git a/packages/walletkit/src/core/EventProcessor.ts b/packages/walletkit/src/core/EventProcessor.ts index d690d0de..0665c8ca 100644 --- a/packages/walletkit/src/core/EventProcessor.ts +++ b/packages/walletkit/src/core/EventProcessor.ts @@ -16,7 +16,7 @@ import type { } from '../types/durableEvents'; import type { EventType } from '../types/internal'; import type { WalletManager } from './WalletManager'; -import type { SessionManager } from './SessionManager'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; import type { EventRouter } from './EventRouter'; import type { EventEmitter } from './EventEmitter'; import { globalLogger } from './Logger'; @@ -35,7 +35,7 @@ export interface EventProcessorConfig { export class StorageEventProcessor implements IEventProcessor { private eventStore: EventStore; private config: DurableEventsConfig; - private sessionManager: SessionManager; + private sessionManager: TONConnectSessionManager; private eventRouter: EventRouter; private eventEmitter: EventEmitter; private walletManager: WalletManager; @@ -58,7 +58,7 @@ export class StorageEventProcessor implements IEventProcessor { eventStore: EventStore, config: DurableEventsConfig, walletManager: WalletManager, - sessionManager: SessionManager, + sessionManager: TONConnectSessionManager, eventRouter: EventRouter, eventEmitter: EventEmitter, ) { @@ -145,7 +145,7 @@ export class StorageEventProcessor implements IEventProcessor { private async processNextAvailableEvent(): Promise { try { // Get all active sessions for registered wallets - const allLocalSessions = this.sessionManager.getSessionsForAPI(); + const allLocalSessions = await this.sessionManager.getSessions(); const allSessions = allLocalSessions.filter( (session) => session.walletId && this.registeredWallets.has(session.walletId), diff --git a/packages/walletkit/src/core/EventRouter.ts b/packages/walletkit/src/core/EventRouter.ts index 7f7aefb6..921e5f85 100644 --- a/packages/walletkit/src/core/EventRouter.ts +++ b/packages/walletkit/src/core/EventRouter.ts @@ -16,7 +16,7 @@ import { DisconnectHandler } from '../handlers/DisconnectHandler'; import { validateBridgeEvent } from '../validation/events'; import { globalLogger } from './Logger'; import type { EventEmitter } from './EventEmitter'; -import type { SessionManager } from './SessionManager'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; import type { WalletManager } from './WalletManager'; import type { BridgeManager } from './BridgeManager'; import type { AnalyticsManager } from '../analytics'; @@ -46,7 +46,7 @@ export class EventRouter { constructor( private config: TonWalletKitOptions, private eventEmitter: EventEmitter, - private sessionManager: SessionManager, + private sessionManager: TONConnectSessionManager, private walletManager: WalletManager, private analyticsManager?: AnalyticsManager, ) { diff --git a/packages/walletkit/src/core/Initializer.ts b/packages/walletkit/src/core/Initializer.ts index 00cf8f7f..60cb96e2 100644 --- a/packages/walletkit/src/core/Initializer.ts +++ b/packages/walletkit/src/core/Initializer.ts @@ -13,7 +13,8 @@ import { DEFAULT_DURABLE_EVENTS_CONFIG } from '../types'; import type { StorageAdapter, StorageConfig } from '../storage'; import { createStorageAdapter, Storage } from '../storage'; import { WalletManager } from './WalletManager'; -import { SessionManager } from './SessionManager'; +import { TONConnectStoredSessionManager } from './TONConnectStoredSessionManager'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; import { BridgeManager } from './BridgeManager'; import { EventRouter } from './EventRouter'; import { RequestProcessor } from './RequestProcessor'; @@ -35,7 +36,7 @@ const log = globalLogger.createChild('Initializer'); */ export interface InitializationResult { walletManager: WalletManager; - sessionManager: SessionManager; + sessionManager: TONConnectSessionManager; bridgeManager: BridgeManager; eventRouter: EventRouter; requestProcessor: RequestProcessor; @@ -133,7 +134,7 @@ export class Initializer { storage: Storage, ): Promise<{ walletManager: WalletManager; - sessionManager: SessionManager; + sessionManager: TONConnectSessionManager; bridgeManager: BridgeManager; eventRouter: EventRouter; eventProcessor: StorageEventProcessor; @@ -142,8 +143,15 @@ export class Initializer { const walletManager = new WalletManager(storage); await walletManager.initialize(); - const sessionManager = new SessionManager(storage, walletManager); - await sessionManager.initialize(); + // Use provided session manager or create default one + let sessionManager: TONConnectSessionManager; + if (options.sessionManager) { + sessionManager = options.sessionManager; + } else { + const storedSessionManager = new TONConnectStoredSessionManager(storage, walletManager); + await storedSessionManager.initialize(); + sessionManager = storedSessionManager; + } const eventStore = new StorageEventStore(storage); const eventRouter = new EventRouter( @@ -200,7 +208,7 @@ export class Initializer { * Initialize processors */ private initializeProcessors( - sessionManager: SessionManager, + sessionManager: TONConnectSessionManager, bridgeManager: BridgeManager, walletManager: WalletManager, ): { diff --git a/packages/walletkit/src/core/RequestProcessor.ts b/packages/walletkit/src/core/RequestProcessor.ts index 36c19ed1..7d77dffa 100644 --- a/packages/walletkit/src/core/RequestProcessor.ts +++ b/packages/walletkit/src/core/RequestProcessor.ts @@ -23,12 +23,13 @@ import { CHAIN, CONNECT_EVENT_ERROR_CODES, SEND_TRANSACTION_ERROR_CODES, + SessionCrypto, SIGN_DATA_ERROR_CODES, } from '@tonconnect/protocol'; import { getSecureRandomBytes } from '@ton/crypto'; import type { EventSignDataApproval, TonWalletKitOptions } from '../types'; -import type { SessionManager } from './SessionManager'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; import type { BridgeManager } from './BridgeManager'; import { globalLogger } from './Logger'; import { CreateTonProofMessage } from '../utils/tonProof'; @@ -62,7 +63,7 @@ export class RequestProcessor { constructor( private walletKitOptions: TonWalletKitOptions, - private sessionManager: SessionManager, + private sessionManager: TONConnectSessionManager, private bridgeManager: BridgeManager, private walletManager: WalletManager, analyticsManager?: AnalyticsManager, @@ -124,18 +125,16 @@ export class RequestProcessor { } // Create session for this connection' - const url = new URL(event.preview.dAppInfo?.url || ''); - const domain = url.host; const newSession = await this.sessionManager.createSession( event.from || (await getSecureRandomBytes(32)).toString('hex'), - event.preview.dAppInfo?.name || '', - domain, - event.preview.dAppInfo?.iconUrl || '', - event.preview.dAppInfo?.description || '', - wallet, { - isJsBridge: event.isJsBridge, + name: event.preview.dAppInfo?.name || '', + url: event.preview.dAppInfo?.url || '', + iconUrl: event.preview.dAppInfo?.iconUrl || '', + description: event.preview.dAppInfo?.description || '', }, + wallet, + event.isJsBridge ?? false, ); // Create bridge session await this.bridgeManager.createSession(newSession.sessionId); @@ -201,16 +200,18 @@ export class RequestProcessor { throw error; } + const isJsBridge = false; // If event is EventConnectApproval, we need to send response to dApp and create session - const url = new URL(event.result.dAppUrl); - const domain = url.host; await this.sessionManager.createSession( event.from || (await getSecureRandomBytes(32)).toString('hex'), - event.result.dAppName, - domain, - event.result.dAppIconUrl, - event.result.dAppDescription, + { + name: event.result.dAppName, + url: event.result.dAppUrl, + iconUrl: event.result.dAppIconUrl, + description: event.result.dAppDescription, + }, wallet, + isJsBridge, ); await this.bridgeManager.sendResponse(event, event.result.response); @@ -292,26 +293,17 @@ export class RequestProcessor { message: reason || 'User rejected connection', }, }; - const newSession = await this.sessionManager.createSession( - event.from || '', - event.preview.dAppInfo?.name || '', - '', - '', - '', - undefined, - { - disablePersist: true, - }, - ); + + const sessionId = event.from || ''; try { - await this.bridgeManager.sendResponse(event, response, newSession); + await this.bridgeManager.sendResponse(event, response, new SessionCrypto()); } catch (error) { log.error('Failed to send connect request rejection response', { error }); } if (this.analytics) { - const sessionData = event.from ? await this.sessionManager.getSession(newSession.sessionId) : undefined; + const sessionData = event.from ? await this.sessionManager.getSession(sessionId) : undefined; // Send wallet-sign-data-request-received event this.analytics.emitWalletConnectRejected({ diff --git a/packages/walletkit/src/core/SessionManager.ts b/packages/walletkit/src/core/TONConnectStoredSessionManager.ts similarity index 51% rename from packages/walletkit/src/core/SessionManager.ts rename to packages/walletkit/src/core/TONConnectStoredSessionManager.ts index 3a873d4a..6394e8ab 100644 --- a/packages/walletkit/src/core/SessionManager.ts +++ b/packages/walletkit/src/core/TONConnectStoredSessionManager.ts @@ -10,23 +10,24 @@ import { SessionCrypto } from '@tonconnect/protocol'; -import type { SessionInfo } from '../types'; import type { WalletManager } from '../core/WalletManager'; -import type { SessionData } from '../types/internal'; import type { Storage } from '../storage'; import { globalLogger } from './Logger'; import type { WalletId } from '../utils/walletId'; -import { createWalletId } from '../utils/walletId'; import type { Wallet } from '../api/interfaces'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; +import type { DAppInfo, TONConnectSession } from '../api/models'; -const log = globalLogger.createChild('SessionManager'); +const log = globalLogger.createChild('TONConnectStoredSessionManager'); -export class SessionManager { - private sessions: Map = new Map(); +export class TONConnectStoredSessionManager implements TONConnectSessionManager { + private sessions: Map = new Map(); private storage: Storage; private walletManager: WalletManager; private storageKey = 'sessions'; + private schemaVersion = 1; + constructor(storage: Storage, walletManager: WalletManager) { this.storage = storage; this.walletManager = walletManager; @@ -37,100 +38,68 @@ export class SessionManager { */ async initialize(): Promise { await this.loadSessions(); + await this.migrateSessions(); } /** * Create new session + * @param sessionId - Unique session identifier + * @param dAppInfo - Information about the dApp * @param wallet - The wallet to associate with this session (optional for connect requests before wallet selection) + * @param options - Additional options for session creation */ async createSession( sessionId: string, - dAppName: string, - domain: string, - dAppIconUrl: string, - dAppDescription: string, - wallet?: Wallet, - { disablePersist = false, isJsBridge = false }: { disablePersist?: boolean; isJsBridge?: boolean } = {}, - ): Promise { + dAppInfo: DAppInfo, + wallet: Wallet, + isJsBridge: boolean, + ): Promise { const now = new Date(); - // const randomKeyPair = keyPairFromSeed(Buffer.from(crypto.getRandomValues(new Uint8Array(32)))); const randomKeyPair = new SessionCrypto().stringifyKeypair(); // Create walletId from wallet if provided - const walletId = wallet ? createWalletId(wallet.getNetwork(), wallet.getAddress()) : ''; + const walletId = wallet.getWalletId(); + + let domain: string; - const sessionData: SessionData = { + try { + const url: URL = new URL(dAppInfo.url || ''); + domain = url.host; + } catch { + throw new Error('Unable to resolve domain from dApp URL for new sessions'); + } + + const session: TONConnectSession = { sessionId, - dAppName, - domain, walletId, walletAddress: wallet?.getAddress() ?? '', createdAt: now.toISOString(), lastActivityAt: now.toISOString(), privateKey: randomKeyPair.secretKey, publicKey: randomKeyPair.publicKey, - dAppIconUrl: dAppIconUrl, - dAppDescription: dAppDescription, + domain: domain, + dAppName: dAppInfo.name, + dAppDescription: dAppInfo.description, + dAppUrl: dAppInfo.url, + dAppIconUrl: dAppInfo.iconUrl, isJsBridge, + schemaVersion: this.schemaVersion, }; - if (disablePersist) { - return SessionManager.toSessionData(sessionData); - } - this.sessions.set(sessionId, sessionData); + this.sessions.set(sessionId, session); await this.persistSessions(); return (await this.getSession(sessionId))!; } - static toSessionData(session: SessionData): SessionData { - return { - sessionId: session.sessionId, - dAppName: session.dAppName, - walletId: session.walletId, - walletAddress: session.walletAddress, - privateKey: session.privateKey, - publicKey: session.publicKey, - createdAt: session.createdAt, - lastActivityAt: session.lastActivityAt, - domain: session.domain, - dAppIconUrl: session.dAppIconUrl, - dAppDescription: session.dAppDescription, - isJsBridge: session.isJsBridge, - }; - } - - // async getSessionData(sessionId: string): Promise {} - /** * Get session by ID */ - async getSession(sessionId: string): Promise { - const session = this.sessions.get(sessionId); - if (session) { - return { - sessionId: session.sessionId, - dAppName: session.dAppName, - walletId: session.walletId, - walletAddress: session.walletAddress, - privateKey: session.privateKey, - publicKey: session.publicKey, - createdAt: session.createdAt, - lastActivityAt: session.lastActivityAt, - domain: session.domain, - dAppIconUrl: session.dAppIconUrl, - dAppDescription: session.dAppDescription, - isJsBridge: session.isJsBridge, - }; - } - return undefined; + async getSession(sessionId: string): Promise { + return this.sessions.get(sessionId); } - async getSessionByDomain(domain: string): Promise { - // const session = this.sessions(domain); - // if (session) { - // return this.getSession(session.sessionId); - // } + async getSessionByDomain(domain: string): Promise { let host; try { host = new URL(domain).host; @@ -148,15 +117,15 @@ export class SessionManager { /** * Get all sessions as array */ - getSessions(): SessionData[] { + async getSessions(): Promise { return Array.from(this.sessions.values()); } /** * Get sessions for specific wallet by wallet ID */ - getSessionsForWallet(walletId: WalletId): SessionData[] { - return this.getSessions().filter((session) => session.walletId === walletId); + async getSessionsForWallet(walletId: WalletId): Promise { + return (await this.getSessions()).filter((session) => session.walletId === walletId); } /** @@ -173,23 +142,18 @@ export class SessionManager { /** * Remove session by ID */ - async removeSession(sessionId: string): Promise { + async removeSession(sessionId: string): Promise { const removed = this.sessions.delete(sessionId); if (removed) { await this.persistSessions(); } - return removed; } /** * Remove all sessions for a wallet by wallet ID or wallet adapter */ - async removeSessionsForWallet(walletOrId: WalletId | Wallet): Promise { - const walletId = - typeof walletOrId === 'string' - ? walletOrId - : createWalletId(walletOrId.getNetwork(), walletOrId.getAddress()); - const sessionsToRemove = this.getSessionsForWallet(walletId); + async removeSessionsForWallet(walletId: WalletId): Promise { + const sessionsToRemove = await this.getSessionsForWallet(walletId); let removedCount = 0; for (const session of sessionsToRemove) { @@ -201,8 +165,6 @@ export class SessionManager { if (removedCount > 0) { await this.persistSessions(); } - - return removedCount; } /** @@ -254,29 +216,15 @@ export class SessionManager { return sessionsToRemove.length; } - /** - * Get sessions as the format expected by the main API - */ - getSessionsForAPI(): Array { - return this.getSessions().map((session) => ({ - sessionId: session.sessionId, - dAppName: session.dAppName, - walletId: session.walletId, - walletAddress: session.walletAddress, - dAppUrl: session.domain, - dAppIconUrl: session.dAppIconUrl, - })); - } - /** * Load sessions from storage */ private async loadSessions(): Promise { try { - const sessionData = await this.storage.get(this.storageKey); + const storedSessions = await this.storage.get(this.storageKey); - if (sessionData && Array.isArray(sessionData)) { - for (const session of sessionData) { + if (storedSessions && Array.isArray(storedSessions)) { + for (const session of storedSessions) { if (session.walletId && !session.walletAddress) { const wallet = this.walletManager.getWallet(session.walletId); if (wallet) { @@ -286,11 +234,9 @@ export class SessionManager { continue; } } - this.sessions.set(session.sessionId, { - ...session, - }); + this.sessions.set(session.sessionId, session); } - log.debug('Loaded session metadata', { count: sessionData.length }); + log.debug('Loaded session metadata', { count: storedSessions.length }); } } catch (error) { log.warn('Failed to load sessions from storage', { error }); @@ -302,25 +248,33 @@ export class SessionManager { */ private async persistSessions(): Promise { try { - // Store session metadata (wallet references need special handling) - const sessionMetadata: SessionData[] = this.getSessions().map((session) => ({ - sessionId: session.sessionId, - dAppName: session.dAppName, - domain: session.domain, - walletId: session.walletId, - walletAddress: session.walletAddress, - createdAt: session.createdAt, - lastActivityAt: session.lastActivityAt, - privateKey: session.privateKey, - publicKey: session.publicKey, - dAppIconUrl: session.dAppIconUrl, - dAppDescription: session.dAppDescription, - isJsBridge: session.isJsBridge, - })); - - await this.storage.set(this.storageKey, sessionMetadata); + const sessionsToStore: TONConnectSession[] = Array.from(this.sessions.values()); + await this.storage.set(this.storageKey, sessionsToStore); } catch (error) { log.warn('Failed to persist sessions to storage', { error }); } } + + private async migrateSessions(): Promise { + for (const [sessionId, session] of this.sessions.entries()) { + const migratedSession = this.migrate(session); + + if (migratedSession) { + this.sessions.set(sessionId, migratedSession); + } else { + this.sessions.delete(sessionId); + } + } + + await this.persistSessions(); + } + + private migrate(session: TONConnectSession): TONConnectSession | undefined { + if (session.schemaVersion === this.schemaVersion) { + return session; + } + + // Currently there is no session versions other that 1, so we just return undefined for all unknown or undefined versions + return undefined; + } } diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index f1ee70e8..a34fc03b 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -16,14 +16,14 @@ import type { DisconnectEvent, SendTransactionRpcResponseError, } from '@tonconnect/protocol'; -import { CHAIN } from '@tonconnect/protocol'; +import { CHAIN, SessionCrypto } from '@tonconnect/protocol'; -import type { ITonWalletKit, TonWalletKitOptions, SessionInfo } from '../types'; +import type { ITonWalletKit, TonWalletKitOptions } from '../types'; import { Initializer, wrapWalletInterface } from './Initializer'; import type { InitializationResult } from './Initializer'; import { globalLogger } from './Logger'; import type { WalletManager } from './WalletManager'; -import type { SessionManager } from './SessionManager'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; import type { EventRouter } from './EventRouter'; import type { RequestProcessor } from './RequestProcessor'; import { JettonsManager } from './JettonsManager'; @@ -58,6 +58,7 @@ import type { ConnectionRequestEvent, TransactionApprovalResponse, SignDataApprovalResponse, + TONConnectSession, } from '../api/models'; import { asAddressFriendly } from '../utils'; @@ -77,7 +78,7 @@ const log = globalLogger.createChild('TonWalletKit'); export class TonWalletKit implements ITonWalletKit { // Component references private walletManager!: WalletManager; - private sessionManager!: SessionManager; + private sessionManager!: TONConnectSessionManager; private eventRouter!: EventRouter; private requestProcessor!: RequestProcessor; // private responseHandler!: ResponseHandler; @@ -352,6 +353,11 @@ export class TonWalletKit implements ITonWalletKit { if (session) { try { + const sessionCrypto = new SessionCrypto({ + publicKey: session.publicKey, + secretKey: session.privateKey, + }); + // For HTTP bridge sessions, send as a response await CallForSuccess( () => @@ -367,7 +373,7 @@ export class TonWalletKit implements ITonWalletKit { id: Date.now(), payload: {}, } as DisconnectEvent, - session, + sessionCrypto, ), 10, 100, @@ -386,7 +392,7 @@ export class TonWalletKit implements ITonWalletKit { log.error('Failed to remove session', { sessionId, error }); } } else { - const sessions = this.sessionManager.getSessions(); + const sessions = await this.sessionManager.getSessions(); if (sessions.length > 0) { for (const session of sessions) { try { @@ -399,9 +405,9 @@ export class TonWalletKit implements ITonWalletKit { } } - async listSessions(): Promise { + async listSessions(): Promise { await this.ensureInitialized(); - return this.sessionManager.getSessionsForAPI(); + return await this.sessionManager.getSessions(); } // === Event Handler Registration (Delegated) === diff --git a/packages/walletkit/src/handlers/DisconnectHandler.ts b/packages/walletkit/src/handlers/DisconnectHandler.ts index 31075106..6c57f1f8 100644 --- a/packages/walletkit/src/handlers/DisconnectHandler.ts +++ b/packages/walletkit/src/handlers/DisconnectHandler.ts @@ -8,7 +8,7 @@ // Disconnect event handler -import type { SessionManager } from '../core/SessionManager'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; import type { RawBridgeEvent, EventHandler, RawBridgeEventDisconnect } from '../types/internal'; import { BasicHandler } from './BasicHandler'; import { WalletKitError, ERROR_CODES } from '../errors'; @@ -20,7 +20,7 @@ export class DisconnectHandler { constructor( notify: (event: DisconnectionEvent) => void, - private readonly sessionManager: SessionManager, + private readonly sessionManager: TONConnectSessionManager, ) { super(notify); } diff --git a/packages/walletkit/src/handlers/SignDataHandler.ts b/packages/walletkit/src/handlers/SignDataHandler.ts index 542e408a..1b98b81e 100644 --- a/packages/walletkit/src/handlers/SignDataHandler.ts +++ b/packages/walletkit/src/handlers/SignDataHandler.ts @@ -20,7 +20,7 @@ import type { WalletManager } from '../core/WalletManager'; import type { SignDataPayload, SignData, SignDataRequestEvent, SignDataPreview, Base64String } from '../api/models'; import { Network } from '../api/models'; import type { Analytics, AnalyticsManager } from '../analytics'; -import type { SessionManager } from '../core/SessionManager'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; const log = globalLogger.createChild('SignDataHandler'); @@ -30,12 +30,12 @@ export class SignDataHandler { private analytics?: Analytics; private walletManager: WalletManager; - private sessionManager: SessionManager; + private sessionManager: TONConnectSessionManager; constructor( notify: (event: SignDataRequestEvent) => void, walletManager: WalletManager, - sessionManager: SessionManager, + sessionManager: TONConnectSessionManager, analyticsManager?: AnalyticsManager, ) { super(notify); diff --git a/packages/walletkit/src/handlers/TransactionHandler.ts b/packages/walletkit/src/handlers/TransactionHandler.ts index a84835c5..3cf14bac 100644 --- a/packages/walletkit/src/handlers/TransactionHandler.ts +++ b/packages/walletkit/src/handlers/TransactionHandler.ts @@ -32,7 +32,7 @@ import type { Wallet } from '../api/interfaces'; import type { TransactionEmulatedPreview, TransactionRequest, TransactionRequestEvent } from '../api/models'; import { Result } from '../api/models'; import type { Analytics, AnalyticsManager } from '../analytics'; -import type { SessionManager } from '../core/SessionManager'; +import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; const log = globalLogger.createChild('TransactionHandler'); @@ -48,7 +48,7 @@ export class TransactionHandler private readonly config: TonWalletKitOptions, eventEmitter: EventEmitter, private readonly walletManager: WalletManager, - private readonly sessionManager: SessionManager, + private readonly sessionManager: TONConnectSessionManager, analyticsManager?: AnalyticsManager, ) { super(notify); diff --git a/packages/walletkit/src/index.ts b/packages/walletkit/src/index.ts index 075ee953..36d9b4bb 100644 --- a/packages/walletkit/src/index.ts +++ b/packages/walletkit/src/index.ts @@ -12,7 +12,8 @@ export * from './types'; export type * from './types/internal'; export * from './errors'; export { WalletManager } from './core/WalletManager'; -export { SessionManager } from './core/SessionManager'; +export { TONConnectStoredSessionManager } from './core/TONConnectStoredSessionManager'; +export type { TONConnectSessionManager } from './api/interfaces/TONConnectSessionManager'; export { BridgeManager } from './core/BridgeManager'; export { EventRouter } from './core/EventRouter'; export { RequestProcessor } from './core/RequestProcessor'; diff --git a/packages/walletkit/src/types/config.ts b/packages/walletkit/src/types/config.ts index f665deff..d33960c6 100644 --- a/packages/walletkit/src/types/config.ts +++ b/packages/walletkit/src/types/config.ts @@ -14,6 +14,7 @@ import type { DeviceInfo, WalletInfo } from './jsBridge'; import type { BridgeConfig } from './internal'; import type { ApiClient } from './toncenter/ApiClient'; import type { AnalyticsManagerOptions } from '../analytics'; +import type { TONConnectSessionManager } from '../api/interfaces'; /** * API client configuration options @@ -46,6 +47,12 @@ export interface TonWalletKitOptions { walletManifest?: WalletInfo; deviceInfo?: DeviceInfo; + /** + * Custom session manager implementation. + * If not provided, TONConnectStoredSessionManager will be used. + */ + sessionManager?: TONConnectSessionManager; + /** * Network configuration */ diff --git a/packages/walletkit/src/types/index.ts b/packages/walletkit/src/types/index.ts index 3b7356c9..0266bfba 100644 --- a/packages/walletkit/src/types/index.ts +++ b/packages/walletkit/src/types/index.ts @@ -19,10 +19,10 @@ export type { EventTransactionApproval, EventSignDataApproval } from './events'; export type { TonWalletKitOptions, NetworkConfig, NetworkAdapters, ApiClientConfig } from './config'; // Main kit interface -export type { ITonWalletKit, SessionInfo } from './kit'; +export type { ITonWalletKit } from './kit'; // Internal types (re-export from internal.ts) -export type { SessionData, BridgeConfig, EventCallback, RawBridgeEvent, EventType, EventHandler } from './internal'; +export type { BridgeConfig, EventCallback, RawBridgeEvent, EventType, EventHandler } from './internal'; // Durable events types export type { EventStatus, StoredEvent, DurableEventsConfig, EventStore, EventProcessor } from './durableEvents'; diff --git a/packages/walletkit/src/types/internal.ts b/packages/walletkit/src/types/internal.ts index 440e16ac..fbbd2bce 100644 --- a/packages/walletkit/src/types/internal.ts +++ b/packages/walletkit/src/types/internal.ts @@ -32,25 +32,6 @@ import { asAddressFriendly } from '../utils/address'; // import type { WalletInterface } from './wallet'; -export interface SessionData { - sessionId: string; - - walletId: WalletId; - walletAddress: UserFriendlyAddress; - createdAt: string; // date - lastActivityAt: string; // date - privateKey: string; - publicKey: string; - - dAppName: string; - dAppDescription: string; - domain: string; - dAppIconUrl: string; - - // Bridge type indicator (needed to determine how to send disconnect events) - isJsBridge?: boolean; // true if session was created via JS Bridge, false/undefined for HTTP Bridge -} - export interface BridgeConfig { bridgeUrl?: string; // defaults to WalletInfo.bridgeUrl if exists enableJsBridge?: boolean; // default to true if WalletInfo.jsBridgeKey exists diff --git a/packages/walletkit/src/types/kit.ts b/packages/walletkit/src/types/kit.ts index cc62b946..14c51f66 100644 --- a/packages/walletkit/src/types/kit.ts +++ b/packages/walletkit/src/types/kit.ts @@ -24,6 +24,7 @@ import type { ConnectionRequestEvent, TransactionApprovalResponse, SignDataApprovalResponse, + TONConnectSession, } from '../api/models'; /** @@ -70,7 +71,7 @@ export interface ITonWalletKit { disconnect(sessionId?: string): Promise; /** List all active sessions */ - listSessions(): Promise; + listSessions(): Promise; // === URL Processing === @@ -135,29 +136,3 @@ export interface ITonWalletKit { /** Jettons API access */ jettons: JettonsAPI; } - -/** - * Session information for API responses - */ -export interface SessionInfo { - /** Unique session identifier */ - sessionId: string; - - /** Connected dApp name */ - dAppName: string; - - /** Connected dApp URL */ - dAppUrl: string; - - /** Connected dApp icon URL */ - dAppIconUrl: string; - - /** Wallet ID */ - walletId: string; - - /** Session creation time */ - createdAt?: Date; - - /** Last activity time */ - lastActivity?: Date; -}