From 0b7093b200a82b06db4b32d3e415a18f8d5edf45 Mon Sep 17 00:00:00 2001 From: Nikita Rodionov Date: Fri, 20 Mar 2026 15:09:57 +0400 Subject: [PATCH 1/2] Allow to create connection event without sending to event handler --- packages/walletkit/src/core/TonWalletKit.ts | 35 +++++++++++++++++++++ packages/walletkit/src/types/kit.ts | 7 +++++ 2 files changed, 42 insertions(+) diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index 2f7eb742a..22cb59039 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -29,6 +29,7 @@ import type { EventRouter } from './EventRouter'; import type { RequestProcessor } from './RequestProcessor'; import { JettonsManager } from './JettonsManager'; import type { JettonsAPI } from '../types/jettons'; +import { ConnectHandler } from '../handlers/ConnectHandler'; import { SwapManager } from '../defi/swap'; import type { RawBridgeEventConnect, @@ -522,6 +523,40 @@ export class TonWalletKit implements ITonWalletKit { this.eventRouter.removeErrorCallback(); } + // === URL Parsing API === + + /** + * Allow to convert url to ConnectionRequestEvent to use inline way + */ + async connectionEventFromUrl(url: string): Promise { + await this.ensureInitialized(); + + try { + const parsedUrl = this.parseTonConnectUrl(url); + if (!parsedUrl) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Invalid TON Connect URL format', undefined, { + url, + }); + } + + const bridgeEvent = this.createConnectEventFromUrl(parsedUrl); + if (!bridgeEvent) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Invalid TON Connect URL - unable to create bridge event', + undefined, + { parsedUrl }, + ); + } + + const handler = new ConnectHandler(() => {}, this.config, this.analyticsManager); + return await handler.handle(bridgeEvent); + } catch (error) { + log.error('Failed to create connection event from URL', { error, url }); + throw error; + } + } + // === URL Processing API === /** diff --git a/packages/walletkit/src/types/kit.ts b/packages/walletkit/src/types/kit.ts index 6159043e2..308791a4e 100644 --- a/packages/walletkit/src/types/kit.ts +++ b/packages/walletkit/src/types/kit.ts @@ -75,6 +75,13 @@ export interface ITonWalletKit { /** List all active sessions */ listSessions(): Promise; + // === URL Parsing API === + + /** + * Allow to convert url to ConnectionRequestEvent to use inline way + */ + connectionEventFromUrl(url: string): Promise; + // === URL Processing === /** Handle pasted TON Connect URL/link */ From cb071df1d57668bddf0cbc794db8f0888535c919 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 20 Mar 2026 15:16:18 +0300 Subject: [PATCH 2/2] draft: alternative handle event implementation --- packages/walletkit/src/core/EventRouter.ts | 88 ++++++++++++++++----- packages/walletkit/src/core/TonWalletKit.ts | 79 +++++++++--------- 2 files changed, 112 insertions(+), 55 deletions(-) diff --git a/packages/walletkit/src/core/EventRouter.ts b/packages/walletkit/src/core/EventRouter.ts index d33134670..6004a9869 100644 --- a/packages/walletkit/src/core/EventRouter.ts +++ b/packages/walletkit/src/core/EventRouter.ts @@ -8,6 +8,8 @@ // Event routing and handler coordination +import type { WalletResponseTemplateError } from '@tonconnect/protocol'; + import type { RawBridgeEvent, EventHandler, EventCallback, EventType } from '../types/internal'; import { ConnectHandler } from '../handlers/ConnectHandler'; import { TransactionHandler } from '../handlers/TransactionHandler'; @@ -32,6 +34,23 @@ import type { TonWalletKitOptions } from '../types/config'; const log = globalLogger.createChild('EventRouter'); +export type DispatchBridgeEventSuccess = { + ok: true; + bridgeEvent: BridgeEvent; + notify: () => Promise; +}; + +export type DispatchBridgeEventFailure = + | { ok: false; reason: 'invalid_event' } + | { ok: false; reason: 'no_handler' } + | { + ok: false; + rawEvent: RawBridgeEvent; + errorResult: WalletResponseTemplateError & { id: string }; + }; + +export type DispatchBridgeEventResult = DispatchBridgeEventSuccess | DispatchBridgeEventFailure; + export class EventRouter { private handlers: EventHandler[] = []; private bridgeManager!: BridgeManager; @@ -58,34 +77,65 @@ export class EventRouter { } /** - * Route incoming bridge event to appropriate handler + * Run validation and handler.handle without notifying listeners or sending bridge responses. + * Use {@link routeEvent} for the full path including notify + error responses. */ - async routeEvent(event: RawBridgeEvent): Promise { - // Validate event structure + async handleBridgeEvent(event: RawBridgeEvent): Promise { const validation = validateBridgeEvent(event); if (!validation.isValid) { log.error('Invalid bridge event', { errors: validation.errors }); - return; + return { ok: false, reason: 'invalid_event' }; } + for (const handler of this.handlers) { + if (handler.canHandle(event)) { + const result = await handler.handle(event); + if ('error' in result) { + return { + ok: false, + rawEvent: event, + errorResult: result as WalletResponseTemplateError & { id: string }, + }; + } + const bridgeEvent = result as BridgeEvent; + return { + ok: true, + bridgeEvent, + notify: () => handler.notify(bridgeEvent), + }; + } + } + + return { ok: false, reason: 'no_handler' }; + } + + /** + * Route incoming bridge event to appropriate handler + */ + async routeEvent(event: RawBridgeEvent): Promise { try { - // Find appropriate handler - for (const handler of this.handlers) { - if (handler.canHandle(event)) { - const result = await handler.handle(event); - if ('error' in result) { - this.notifyErrorCallback({ id: result.id, data: { ...event }, error: result.error }); - try { - await this.bridgeManager.sendResponse(event, result); - } catch (error) { - log.error('Error sending response for error event', { error, event, result }); - } - return; - } - await handler.notify(result as BridgeEvent); - break; + const dispatched = await this.handleBridgeEvent(event); + if (!dispatched.ok) { + if ('reason' in dispatched) { + return; + } + this.notifyErrorCallback({ + id: dispatched.errorResult.id, + data: { ...dispatched.rawEvent }, + error: dispatched.errorResult.error, + }); + try { + await this.bridgeManager.sendResponse(dispatched.rawEvent, dispatched.errorResult); + } catch (error) { + log.error('Error sending response for error event', { + error, + event: dispatched.rawEvent, + result: dispatched.errorResult, + }); } + return; } + await dispatched.notify(); } catch (error) { log.error('Error routing event', { error }); throw error; diff --git a/packages/walletkit/src/core/TonWalletKit.ts b/packages/walletkit/src/core/TonWalletKit.ts index 22cb59039..8a6b85de0 100644 --- a/packages/walletkit/src/core/TonWalletKit.ts +++ b/packages/walletkit/src/core/TonWalletKit.ts @@ -29,7 +29,6 @@ import type { EventRouter } from './EventRouter'; import type { RequestProcessor } from './RequestProcessor'; import { JettonsManager } from './JettonsManager'; import type { JettonsAPI } from '../types/jettons'; -import { ConnectHandler } from '../handlers/ConnectHandler'; import { SwapManager } from '../defi/swap'; import type { RawBridgeEventConnect, @@ -532,25 +531,26 @@ export class TonWalletKit implements ITonWalletKit { await this.ensureInitialized(); try { - const parsedUrl = this.parseTonConnectUrl(url); - if (!parsedUrl) { - throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Invalid TON Connect URL format', undefined, { + const bridgeEvent = this.connectBridgeEventFromTonConnectUrl(url); + + const dispatched = await this.eventRouter.handleBridgeEvent(bridgeEvent); + if (!dispatched.ok) { + if ('reason' in dispatched) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + dispatched.reason === 'invalid_event' + ? 'Invalid TON Connect bridge event' + : 'No handler for TON Connect bridge event', + undefined, + { url, reason: dispatched.reason }, + ); + } + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'TON Connect request failed', undefined, { url, + error: dispatched.errorResult, }); } - - const bridgeEvent = this.createConnectEventFromUrl(parsedUrl); - if (!bridgeEvent) { - throw new WalletKitError( - ERROR_CODES.VALIDATION_ERROR, - 'Invalid TON Connect URL - unable to create bridge event', - undefined, - { parsedUrl }, - ); - } - - const handler = new ConnectHandler(() => {}, this.config, this.analyticsManager); - return await handler.handle(bridgeEvent); + return dispatched.bridgeEvent as ConnectionRequestEvent; } catch (error) { log.error('Failed to create connection event from URL', { error, url }); throw error; @@ -567,25 +567,7 @@ export class TonWalletKit implements ITonWalletKit { await this.ensureInitialized(); try { - // Parse and validate the TON Connect URL - const parsedUrl = this.parseTonConnectUrl(url); - if (!parsedUrl) { - throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Invalid TON Connect URL format', undefined, { - url, - }); - } - - // Create a bridge event from the parsed URL - const bridgeEvent = this.createConnectEventFromUrl(parsedUrl); - if (!bridgeEvent) { - throw new WalletKitError( - ERROR_CODES.VALIDATION_ERROR, - 'Invalid TON Connect URL - unable to create bridge event', - undefined, - { parsedUrl }, - ); - } - + const bridgeEvent = this.connectBridgeEventFromTonConnectUrl(url); await this.eventRouter.routeEvent(bridgeEvent); } catch (error) { log.error('Failed to handle TON Connect URL', { error, url }); @@ -687,6 +669,31 @@ export class TonWalletKit implements ITonWalletKit { }; } + /** + * Parse a TON Connect link into a raw connect bridge event. + * @throws WalletKitError if the URL is invalid or cannot be turned into a connect event + */ + private connectBridgeEventFromTonConnectUrl(url: string): RawBridgeEventConnect { + const parsedUrl = this.parseTonConnectUrl(url); + if (!parsedUrl) { + throw new WalletKitError(ERROR_CODES.VALIDATION_ERROR, 'Invalid TON Connect URL format', undefined, { + url, + }); + } + + const bridgeEvent = this.createConnectEventFromUrl(parsedUrl); + if (!bridgeEvent) { + throw new WalletKitError( + ERROR_CODES.VALIDATION_ERROR, + 'Invalid TON Connect URL - unable to create bridge event', + undefined, + { parsedUrl }, + ); + } + + return bridgeEvent; + } + // === Request Processing API (Delegated) === async approveConnectRequest(event: ConnectionRequestEvent, response?: ConnectionApprovalResponse): Promise {