From c1203c7c693a35777d41bb51a5dc75eccc693dd5 Mon Sep 17 00:00:00 2001 From: Nikita Rodionov Date: Thu, 26 Mar 2026 12:44:32 +0400 Subject: [PATCH] Draft for custom event handling from web view --- packages/walletkit/src/core/BridgeManager.ts | 99 +++++++++++-------- packages/walletkit/src/core/EventRouter.ts | 96 ++++++++++++++++-- .../src/core/TonConnectEventsHandler.ts | 23 +++++ .../src/core/TonConnectEventsRouter.ts | 14 +++ packages/walletkit/src/core/TonWalletKit.ts | 12 ++- packages/walletkit/src/index.ts | 2 + packages/walletkit/src/types/kit.ts | 3 + 7 files changed, 196 insertions(+), 53 deletions(-) create mode 100644 packages/walletkit/src/core/TonConnectEventsHandler.ts create mode 100644 packages/walletkit/src/core/TonConnectEventsRouter.ts diff --git a/packages/walletkit/src/core/BridgeManager.ts b/packages/walletkit/src/core/BridgeManager.ts index 70b1beb5b..1ae905934 100644 --- a/packages/walletkit/src/core/BridgeManager.ts +++ b/packages/walletkit/src/core/BridgeManager.ts @@ -28,6 +28,7 @@ import type { import { uuidv7 } from '../utils/uuid'; import { WalletKitError, ERROR_CODES } from '../errors'; import type { Analytics, AnalyticsManager } from '../analytics'; +import type { TonConnectEventsHandler } from './TonConnectEventsHandler'; import type { TonWalletKitOptions } from '../types/config'; import { TONCONNECT_BRIDGE_RESPONSE } from '../bridge/JSBridgeInjector'; import type { BridgeEvent, TONConnectSession } from '../api/models'; @@ -48,7 +49,7 @@ export class BridgeManager { // Event processing queue and concurrency control // eslint-disable-next-line @typescript-eslint/no-explicit-any - private eventQueue: any[] = []; + private eventQueue: Array<{ event: any; eventsHandler?: TonConnectEventsHandler }> = []; private isProcessing = false; // Durable events support @@ -479,7 +480,7 @@ export class BridgeManager { // eslint-disable-next-line @typescript-eslint/no-explicit-any private queueBridgeEvent(event: any): void { log.debug('Bridge event queued', { eventId: event?.id, event }); - this.eventQueue.push(event); + this.eventQueue.push({ event }); // Trigger processing (don't wait for it to complete) this.processBridgeEvents().catch((error) => { @@ -490,6 +491,7 @@ export class BridgeManager { public queueJsBridgeEvent( messageInfo: BridgeEventMessageInfo, event: InjectedToExtensionBridgeRequestPayload, + eventsHandler?: TonConnectEventsHandler, ): void { log.debug('JS Bridge event queued', { eventId: messageInfo?.messageId }); @@ -504,12 +506,15 @@ export class BridgeManager { if (event.method == 'connect') { this.eventQueue.push({ - ...event, - isJsBridge: true, - tabId: messageInfo.tabId, - domain: messageInfo.domain, - messageId: messageInfo.messageId, - walletId: messageInfo.walletId, + event: { + ...event, + isJsBridge: true, + tabId: messageInfo.tabId, + domain: messageInfo.domain, + messageId: messageInfo.messageId, + walletId: messageInfo.walletId, + }, + eventsHandler, }); } else if (event.method == 'restoreConnection') { this.eventEmitter?.emit('restoreConnection', { @@ -521,14 +526,17 @@ export class BridgeManager { }); } else if (event.method == 'send' && event?.params?.length === 1) { this.eventQueue.push({ - ...event, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(event as any).params[0], - isJsBridge: true, - tabId: messageInfo.tabId, - domain: messageInfo.domain, - messageId: messageInfo.messageId, - walletId: messageInfo.walletId, + event: { + ...event, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...(event as any).params[0], + isJsBridge: true, + tabId: messageInfo.tabId, + domain: messageInfo.domain, + messageId: messageInfo.messageId, + walletId: messageInfo.walletId, + }, + eventsHandler, }); } @@ -557,11 +565,11 @@ export class BridgeManager { try { // Process all events in FIFO order while (this.eventQueue.length > 0) { - const event = this.eventQueue.shift(); - if (event) { + const item = this.eventQueue.shift(); + if (item) { // Important: set isLocal to false for all events from bridge - event.isLocal = false; - await this.handleBridgeEvent(event); + item.event.isLocal = false; + await this.handleBridgeEvent(item.event, item.eventsHandler); } } } catch (error) { @@ -578,7 +586,7 @@ export class BridgeManager { * Handle individual bridge event (original processing logic) */ // eslint-disable-next-line @typescript-eslint/no-explicit-any - private async handleBridgeEvent(event: any): Promise { + private async handleBridgeEvent(event: any, eventsHandler?: TonConnectEventsHandler): Promise { try { log.info('Bridge event received', { event }); // Convert bridge event to our internal format @@ -655,31 +663,36 @@ export class BridgeManager { } } - // Store event durably if enabled - if (!this.eventStore) { - throw new WalletKitError(ERROR_CODES.EVENT_STORE_NOT_INITIALIZED, 'Event store is not initialized'); - } - try { - await this.eventStore.storeEvent(rawEvent); - - // Notify that bridge storage was updated - if (this.eventEmitter) { - this.eventEmitter.emit('bridge-storage-updated'); + if (eventsHandler) { + await this.eventRouter.routeEvent(rawEvent, eventsHandler); + log.info('Event routed directly to custom handler', { eventId: rawEvent.id, method: rawEvent.method }); + } else { + // Store event durably if enabled + if (!this.eventStore) { + throw new WalletKitError(ERROR_CODES.EVENT_STORE_NOT_INITIALIZED, 'Event store is not initialized'); } + try { + await this.eventStore.storeEvent(rawEvent); - log.info('Event stored durably', { eventId: rawEvent.id, method: rawEvent.method }); - } catch (error) { - log.error('Failed to store event durably', { - eventId: rawEvent.id, - error: (error as Error).message, - }); + // Notify that bridge storage was updated + if (this.eventEmitter) { + this.eventEmitter.emit('bridge-storage-updated'); + } - throw WalletKitError.fromError( - ERROR_CODES.EVENT_STORE_OPERATION_FAILED, - 'Failed to store event durably', - error, - { eventId: rawEvent.id, method: rawEvent.method }, - ); + log.info('Event stored durably', { eventId: rawEvent.id, method: rawEvent.method }); + } catch (error) { + log.error('Failed to store event durably', { + eventId: rawEvent.id, + error: (error as Error).message, + }); + + throw WalletKitError.fromError( + ERROR_CODES.EVENT_STORE_OPERATION_FAILED, + 'Failed to store event durably', + error, + { eventId: rawEvent.id, method: rawEvent.method }, + ); + } } log.info('Bridge event processed', { rawEvent }); diff --git a/packages/walletkit/src/core/EventRouter.ts b/packages/walletkit/src/core/EventRouter.ts index d33134670..f078b2336 100644 --- a/packages/walletkit/src/core/EventRouter.ts +++ b/packages/walletkit/src/core/EventRouter.ts @@ -29,12 +29,15 @@ import type { ConnectionRequestEvent, } from '../api/models'; import type { TonWalletKitOptions } from '../types/config'; +import type { TonConnectEventsHandler } from './TonConnectEventsHandler'; +import type { TonConnectEventsRouter } from './TonConnectEventsRouter'; const log = globalLogger.createChild('EventRouter'); -export class EventRouter { +export class EventRouter implements TonConnectEventsRouter { private handlers: EventHandler[] = []; private bridgeManager!: BridgeManager; + private eventsHandlers: Set = new Set(); // Event callbacks private connectRequestCallback: EventCallback | undefined = undefined; @@ -57,10 +60,20 @@ export class EventRouter { this.bridgeManager = bridgeManager; } + // TonConnectEventsRouter + + add(eventsHandler: TonConnectEventsHandler): void { + this.eventsHandlers.add(eventsHandler); + } + + remove(eventsHandler: TonConnectEventsHandler): void { + this.eventsHandlers.delete(eventsHandler); + } + /** * Route incoming bridge event to appropriate handler */ - async routeEvent(event: RawBridgeEvent): Promise { + async routeEvent(event: RawBridgeEvent, eventsHandler?: TonConnectEventsHandler): Promise { // Validate event structure const validation = validateBridgeEvent(event); if (!validation.isValid) { @@ -74,7 +87,12 @@ export class EventRouter { if (handler.canHandle(event)) { const result = await handler.handle(event); if ('error' in result) { - this.notifyErrorCallback({ id: result.id, data: { ...event }, error: result.error }); + const errorEvent = { id: result.id, data: { ...event }, error: result.error }; + if (eventsHandler) { + eventsHandler.handleRequestError(errorEvent); + } else { + this.notifyErrorCallback(errorEvent); + } try { await this.bridgeManager.sendResponse(event, result); } catch (error) { @@ -82,7 +100,11 @@ export class EventRouter { } return; } - await handler.notify(result as BridgeEvent); + if (eventsHandler) { + this.dispatchToEventsHandler(eventsHandler, event.method, result as BridgeEvent); + } else { + await handler.notify(result as BridgeEvent); + } break; } } @@ -92,6 +114,23 @@ export class EventRouter { } } + private dispatchToEventsHandler(eventsHandler: TonConnectEventsHandler, method: string, event: BridgeEvent): void { + switch (method) { + case 'connect': + eventsHandler.handleConnectRequest(event as ConnectionRequestEvent); + break; + case 'sendTransaction': + eventsHandler.handleSendTransactionRequest(event as SendTransactionRequestEvent); + break; + case 'signData': + eventsHandler.handleSignDataRequest(event as SignDataRequestEvent); + break; + case 'disconnect': + eventsHandler.handleDisconnection(event as DisconnectionEvent); + break; + } + } + /** * Register event callbacks */ @@ -177,41 +216,45 @@ export class EventRouter { * Notify connect request callbacks */ private async notifyConnectRequestCallbacks(event: ConnectionRequestEvent): Promise { - return await this.connectRequestCallback?.(event); + return await this.handleConnectRequest(event); } /** * Notify transaction request callbacks */ private async notifyTransactionRequestCallbacks(event: SendTransactionRequestEvent): Promise { - return await this.transactionRequestCallback?.(event); + return await this.handleSendTransactionRequest(event); } /** * Notify sign data request callbacks */ private async notifySignDataRequestCallbacks(event: SignDataRequestEvent): Promise { - return await this.signDataRequestCallback?.(event); + return await this.handleSignDataRequest(event); } /** * Notify disconnect callbacks */ private async notifyDisconnectCallbacks(event: DisconnectionEvent): Promise { - return await this.disconnectCallback?.(event); + return await this.handleDisconnection(event); } /** * Notify error callbacks */ private async notifyErrorCallback(event: RequestErrorEvent): Promise { - return await this.errorCallback?.(event); + return await this.handleRequestError(event); } /** * Get enabled event types based on registered callbacks */ getEnabledEventTypes(): EventType[] { + if (this.eventsHandlers.size > 0) { + return ['connect', 'sendTransaction', 'signData', 'disconnect']; + } + const enabledTypes: EventType[] = []; if (this.connectRequestCallback) { @@ -229,4 +272,39 @@ export class EventRouter { return enabledTypes; } + + handleConnectRequest(event: ConnectionRequestEvent): void | Promise { + this.connectRequestCallback?.(event); + for (const handler of this.eventsHandlers) { + handler.handleConnectRequest(event); + } + } + + handleSendTransactionRequest(event: SendTransactionRequestEvent): void | Promise { + this.transactionRequestCallback?.(event); + for (const handler of this.eventsHandlers) { + handler.handleSendTransactionRequest(event); + } + } + + handleSignDataRequest(event: SignDataRequestEvent): void | Promise { + this.signDataRequestCallback?.(event); + for (const handler of this.eventsHandlers) { + handler.handleSignDataRequest(event); + } + } + + handleDisconnection(event: DisconnectionEvent): void | Promise { + this.disconnectCallback?.(event); + for (const handler of this.eventsHandlers) { + handler.handleDisconnection(event); + } + } + + handleRequestError(event: RequestErrorEvent): void | Promise { + this.errorCallback?.(event); + for (const handler of this.eventsHandlers) { + handler.handleRequestError(event); + } + } } diff --git a/packages/walletkit/src/core/TonConnectEventsHandler.ts b/packages/walletkit/src/core/TonConnectEventsHandler.ts new file mode 100644 index 000000000..349a3e06d --- /dev/null +++ b/packages/walletkit/src/core/TonConnectEventsHandler.ts @@ -0,0 +1,23 @@ +/** + * 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 { + SendTransactionRequestEvent, + RequestErrorEvent, + DisconnectionEvent, + SignDataRequestEvent, + ConnectionRequestEvent, +} from '../api/models'; + +export interface TonConnectEventsHandler { + handleConnectRequest(event: ConnectionRequestEvent): void | Promise; + handleSendTransactionRequest(event: SendTransactionRequestEvent): void | Promise; + handleSignDataRequest(event: SignDataRequestEvent): void | Promise; + handleDisconnection(event: DisconnectionEvent): void | Promise; + handleRequestError(event: RequestErrorEvent): void | Promise; +} diff --git a/packages/walletkit/src/core/TonConnectEventsRouter.ts b/packages/walletkit/src/core/TonConnectEventsRouter.ts new file mode 100644 index 000000000..d92fe078c --- /dev/null +++ b/packages/walletkit/src/core/TonConnectEventsRouter.ts @@ -0,0 +1,14 @@ +/** + * 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 { TonConnectEventsHandler } from './TonConnectEventsHandler'; + +export interface TonConnectEventsRouter { + add(eventsHandler: TonConnectEventsHandler): void; + remove(eventsHandler: TonConnectEventsHandler): void; +} diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index 179fba4af..8b289356d 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -26,6 +26,8 @@ import { globalLogger } from './Logger'; import type { WalletManager } from './WalletManager'; import type { TONConnectSessionManager } from '../api/interfaces/TONConnectSessionManager'; import type { EventRouter } from './EventRouter'; +import type { TonConnectEventsHandler } from './TonConnectEventsHandler'; +import type { TonConnectEventsRouter } from './TonConnectEventsRouter'; import type { RequestProcessor } from './RequestProcessor'; import { JettonsManager } from './JettonsManager'; import type { JettonsAPI } from '../types/jettons'; @@ -815,6 +817,13 @@ export class TonWalletKit implements ITonWalletKit { return this.swapManager; } + /** + * TON Connect events provider + */ + get tonConnect(): TonConnectEventsRouter { + return this.eventRouter; + } + /** * Get the event emitter for this kit instance * Allows external components to listen to and emit events @@ -832,8 +841,9 @@ export class TonWalletKit implements ITonWalletKit { async processInjectedBridgeRequest( messageInfo: BridgeEventMessageInfo, request: InjectedToExtensionBridgeRequestPayload, + eventsHandler?: TonConnectEventsHandler, ): Promise { await this.ensureInitialized(); - return this.bridgeManager.queueJsBridgeEvent(messageInfo, request); + return this.bridgeManager.queueJsBridgeEvent(messageInfo, request, eventsHandler); } } diff --git a/packages/walletkit/src/index.ts b/packages/walletkit/src/index.ts index 23479bc3a..6216b1a1d 100644 --- a/packages/walletkit/src/index.ts +++ b/packages/walletkit/src/index.ts @@ -16,6 +16,8 @@ export { TONConnectStoredSessionManager } from './core/TONConnectStoredSessionMa export type { TONConnectSessionManager } from './api/interfaces/TONConnectSessionManager'; export { BridgeManager } from './core/BridgeManager'; export { EventRouter } from './core/EventRouter'; +export type { TonConnectEventsHandler } from './core/TonConnectEventsHandler'; +export type { TonConnectEventsRouter } from './core/TonConnectEventsRouter'; export { RequestProcessor } from './core/RequestProcessor'; export { Initializer } from './core/Initializer'; export { JettonsManager } from './core/JettonsManager'; diff --git a/packages/walletkit/src/types/kit.ts b/packages/walletkit/src/types/kit.ts index 308791a4e..072c8d2cb 100644 --- a/packages/walletkit/src/types/kit.ts +++ b/packages/walletkit/src/types/kit.ts @@ -28,6 +28,7 @@ import type { ConnectionApprovalResponse, } from '../api/models'; import type { SwapAPI } from '../api/interfaces'; +import type { TonConnectEventsRouter } from '../core/TonConnectEventsRouter'; /** * Main TonWalletKit interface @@ -155,4 +156,6 @@ export interface ITonWalletKit { /** Jettons API access */ swap: SwapAPI; + + tonConnect: TonConnectEventsRouter; }